From 94c41d32ced998662addd81b01a9edd1e33e0346 Mon Sep 17 00:00:00 2001 From: Madeline Kalilh Date: Mon, 10 Feb 2025 11:31:51 -0500 Subject: [PATCH 01/99] gopls/internal/golang: add comment about SymbolKind Change-Id: I99bdf283f0ebd63ab1de044ca54fc8ce65136e65 Reviewed-on: https://go-review.googlesource.com/c/tools/+/648096 Reviewed-by: Hongxiang Jiang LUCI-TryBot-Result: Go LUCI --- gopls/internal/protocol/command/interface.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index 32e03dd388a..34adf59b38e 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go @@ -814,6 +814,9 @@ type PackageSymbol struct { Detail string `json:"detail,omitempty"` + // protocol.SymbolKind maps an integer to an enum: + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind + // i.e. File = 1 Kind protocol.SymbolKind `json:"kind"` Tags []protocol.SymbolTag `json:"tags,omitempty"` From 91bac86b5c14ba7d5ed6033fe1b85d9c2aed8215 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Mon, 10 Feb 2025 16:32:24 -0500 Subject: [PATCH 02/99] internal/analysisinternal: add CanImport Add a function that reports whether one package can import another, respecting the Go toolchain's interpretation of path segments named "internal". Change-Id: I66dc90453a178d0e626117f4e3cadf30e61912dc Reviewed-on: https://go-review.googlesource.com/c/tools/+/648255 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- internal/analysisinternal/analysis.go | 27 +++++++++++++++++ internal/analysisinternal/analysis_test.go | 34 ++++++++++++++++++++++ internal/refactor/inline/inline.go | 27 ++--------------- 3 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 internal/analysisinternal/analysis_test.go diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index abf708111bf..a1edabbe84d 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -449,3 +449,30 @@ func validateFix(fset *token.FileSet, fix *analysis.SuggestedFix) error { return nil } + +// CanImport reports whether one package is allowed to import another. +// +// TODO(adonovan): allow customization of the accessibility relation +// (e.g. for Bazel). +func CanImport(from, to string) bool { + // TODO(adonovan): better segment hygiene. + if to == "internal" || strings.HasPrefix(to, "internal/") { + // Special case: only std packages may import internal/... + // We can't reliably know whether we're in std, so we + // use a heuristic on the first segment. + first, _, _ := strings.Cut(from, "/") + if strings.Contains(first, ".") { + return false // example.com/foo ∉ std + } + if first == "testdata" { + return false // testdata/foo ∉ std + } + } + if strings.HasSuffix(to, "/internal") { + return strings.HasPrefix(from, to[:len(to)-len("/internal")]) + } + if i := strings.LastIndex(to, "/internal/"); i >= 0 { + return strings.HasPrefix(from, to[:i]) + } + return true +} diff --git a/internal/analysisinternal/analysis_test.go b/internal/analysisinternal/analysis_test.go new file mode 100644 index 00000000000..0b21876d386 --- /dev/null +++ b/internal/analysisinternal/analysis_test.go @@ -0,0 +1,34 @@ +// 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 analysisinternal + +import "testing" + +func TestCanImport(t *testing.T) { + for _, tt := range []struct { + from string + to string + want bool + }{ + {"fmt", "internal", true}, + {"fmt", "internal/foo", true}, + {"a.com/b", "internal", false}, + {"a.com/b", "xinternal", true}, + {"a.com/b", "internal/foo", false}, + {"a.com/b", "xinternal/foo", true}, + {"a.com/b", "a.com/internal", true}, + {"a.com/b", "a.com/b/internal", true}, + {"a.com/b", "a.com/b/internal/foo", true}, + {"a.com/b", "a.com/c/internal", false}, + {"a.com/b", "a.com/c/xinternal", true}, + {"a.com/b", "a.com/c/internal/foo", false}, + {"a.com/b", "a.com/c/xinternal/foo", true}, + } { + got := CanImport(tt.from, tt.to) + if got != tt.want { + t.Errorf("CanImport(%q, %q) = %v, want %v", tt.from, tt.to, got, tt.want) + } + } +} diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 2c897c24954..96fbb8f8706 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -23,6 +23,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/imports" + "golang.org/x/tools/internal/analysisinternal" internalastutil "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" @@ -331,7 +332,7 @@ func (st *state) inline() (*Result, error) { for _, imp := range res.newImports { // Check that the new imports are accessible. path, _ := strconv.Unquote(imp.spec.Path.Value) - if !canImport(caller.Types.Path(), path) { + if !analysisinternal.CanImport(caller.Types.Path(), path) { return nil, fmt.Errorf("can't inline function %v as its body refers to inaccessible package %q", callee, path) } importDecl.Specs = append(importDecl.Specs, imp.spec) @@ -3196,30 +3197,6 @@ func last[T any](slice []T) T { return *new(T) } -// canImport reports whether one package is allowed to import another. -// -// TODO(adonovan): allow customization of the accessibility relation -// (e.g. for Bazel). -func canImport(from, to string) bool { - // TODO(adonovan): better segment hygiene. - if strings.HasPrefix(to, "internal/") { - // Special case: only std packages may import internal/... - // We can't reliably know whether we're in std, so we - // use a heuristic on the first segment. - first, _, _ := strings.Cut(from, "/") - if strings.Contains(first, ".") { - return false // example.com/foo ∉ std - } - if first == "testdata" { - return false // testdata/foo ∉ std - } - } - if i := strings.LastIndex(to, "/internal/"); i >= 0 { - return strings.HasPrefix(from, to[:i]) - } - return true -} - // 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 From f61b225cbfefb205c92c7ebcd3e25c20c0f3c090 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Mon, 10 Feb 2025 15:54:38 -0500 Subject: [PATCH 03/99] internal/analysisinternal: AddImport puts new import in a group If AddImport needs to add a new import, and the file's first declaration is a grouped import, then add it to that import. This is one step towards a full implementation of the issue below, and perhaps is good enough. For golang/go#71647. Change-Id: I8327b07c21c3efbd189c519e51c339b7aa4751d8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/648136 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Auto-Submit: Jonathan Amsterdam --- internal/analysisinternal/addimport_test.go | 22 +++++++++++++++++++++ internal/analysisinternal/analysis.go | 22 ++++++++++++++------- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/internal/analysisinternal/addimport_test.go b/internal/analysisinternal/addimport_test.go index 145d5861b8f..12423b7c061 100644 --- a/internal/analysisinternal/addimport_test.go +++ b/internal/analysisinternal/addimport_test.go @@ -219,6 +219,28 @@ import . "fmt" func _(Print fmt.Stringer) { fmt +}`, + }, + { + descr: descr("add import to group"), + src: `package a + +import ( + "io" +) + +func _(io.Reader) { + «fmt fmt» +}`, + want: `package a + +import ( + "io" + "fmt" +) + +func _(io.Reader) { + fmt }`, }, } { diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index a1edabbe84d..d96d22982c5 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -255,16 +255,16 @@ func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member newName = fmt.Sprintf("%s%d", preferredName, i) } - // For now, keep it real simple: create a new import - // declaration before the first existing declaration (which - // must exist), including its comments, and let goimports tidy it up. + // 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("import %q\n\n", pkgpath) + newText := fmt.Sprintf("%q", pkgpath) if newName != preferredName || newName != pathpkg.Base(pkgpath) { - newText = fmt.Sprintf("import %s %q\n\n", newName, pkgpath) + newText = fmt.Sprintf("%s %q", newName, pkgpath) } decl0 := file.Decls[0] var before ast.Node = decl0 @@ -278,9 +278,17 @@ func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member before = decl0.Doc } } + // If the first decl is an import group, add this new import at the end. + if gd, ok := before.(*ast.GenDecl); ok && gd.Tok == token.IMPORT && gd.Rparen.IsValid() { + pos = gd.Rparen + newText = "\t" + newText + "\n" + } else { + pos = before.Pos() + newText = "import " + newText + "\n\n" + } return newName, newName + ".", []analysis.TextEdit{{ - Pos: before.Pos(), - End: before.Pos(), + Pos: pos, + End: pos, NewText: []byte(newText), }} } From 027eab55ae11cdaf85b7e426cd74249b206070a3 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 7 Feb 2025 17:34:36 -0500 Subject: [PATCH 04/99] go/analysis/analysistest: RunWithSuggestedFix: 3-way merge This CL adds support for three-way merging to RunWithSuggestedFix, similar to the recent changes in go/analysis/internal/checker's -fix logic. Although unresolved conflicts are still considered a test failure, the diff algorithm may successfully merge identical edits, in particular redundant imports of the same package. NOTE: This does mean that existing tests whose golden files expect redundant imports will fail, and need updating. The test failure messages are improved, again following internal/checker. Fixes golang/go#67049 Fixes golang/go#68765 Change-Id: I8ace0ff0cd0b147696ec4dc742b0d63d0d713155 Reviewed-on: https://go-review.googlesource.com/c/tools/+/647798 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- go/analysis/analysistest/analysistest.go | 284 ++++++++++-------- go/analysis/internal/checker/fix_test.go | 3 + .../src/slicesdelete/slicesdelete.go.golden | 14 - .../src/sortslice/sortslice.go.golden | 2 - 4 files changed, 164 insertions(+), 139 deletions(-) diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index 775fd20094d..08981776478 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -7,6 +7,7 @@ package analysistest import ( "bytes" + "cmp" "fmt" "go/format" "go/token" @@ -78,16 +79,24 @@ type Testing interface { Errorf(format string, args ...interface{}) } -// RunWithSuggestedFixes behaves like Run, but additionally verifies suggested fixes. -// It uses golden files placed alongside the source code under analysis: -// suggested fixes for code in example.go will be compared against example.go.golden. +// RunWithSuggestedFixes behaves like Run, but additionally applies +// suggested fixes and verifies their output. // -// Golden files can be formatted in one of two ways: as plain Go source code, or as txtar archives. -// In the first case, all suggested fixes will be applied to the original source, which will then be compared against the golden file. -// In the second case, suggested fixes will be grouped by their messages, and each set of fixes will be applied and tested separately. -// Each section in the archive corresponds to a single message. +// It uses golden files, placed alongside each source file, to express +// the desired output: the expected transformation of file example.go +// is specified in file example.go.golden. // -// A golden file using txtar may look like this: +// Golden files may be of two forms: a plain Go source file, or a +// txtar archive. +// +// A plain Go source file indicates the expected result of applying +// all suggested fixes to the original file. +// +// A txtar archive specifies, in each section, the expected result of +// applying all suggested fixes of a given message to the original +// file; the name of the archive section is the fix's message. In this +// way, the various alternative fixes offered by a single diagnostic +// can be tested independently. Here's an example: // // -- turn into single negation -- // package pkg @@ -109,41 +118,28 @@ type Testing interface { // // # Conflicts // -// A single analysis pass may offer two or more suggested fixes that -// (1) conflict but are nonetheless logically composable, (e.g. -// because both update the import declaration), or (2) are -// fundamentally incompatible (e.g. alternative fixes to the same -// statement). -// -// It is up to the driver to decide how to apply such fixes. A -// sophisticated driver could attempt to resolve conflicts of the -// first kind, but this test driver simply reports the fact of the -// conflict with the expectation that the user will split their tests -// into nonconflicting parts. +// Regardless of the form of the golden file, it is possible for +// multiple fixes to conflict, either because they overlap, or are +// close enough together that the particular diff algorithm cannot +// separate them. // -// Conflicts of the second kind can be avoided by giving the -// alternative fixes different names (SuggestedFix.Message) and -// defining the .golden file as a multi-section txtar file with a -// named section for each alternative fix, as shown above. +// RunWithSuggestedFixes uses a simple three-way merge to accumulate +// fixes, similar to a git merge. The merge algorithm may be able to +// coalesce identical edits, for example duplicate imports of the same +// package. (Bear in mind that this is an editorial decision. In +// general, coalescing identical edits may not be correct: consider +// two statements that increment the same counter.) // -// Analyzers that compute fixes from a textual diff of the -// before/after file contents (instead of directly from syntax tree -// positions) may produce fixes that, although logically -// non-conflicting, nonetheless conflict due to the particulars of the -// diff algorithm. In such cases it may suffice to introduce -// sufficient separation of the statements in the test input so that -// the computed diffs do not overlap. If that fails, break the test -// into smaller parts. +// If there are conflicts, the test fails. In any case, the +// non-conflicting edits will be compared against the expected output. +// In this situation, we recommend that you increase the textual +// separation between conflicting parts or, if that fails, split +// your tests into smaller parts. // -// TODO(adonovan): the behavior of RunWithSuggestedFixes as documented -// above is impractical for tests that report multiple diagnostics and -// offer multiple alternative fixes for the same diagnostic, and it is -// inconsistent with the interpretation of multiple diagnostics -// described at Diagnostic.SuggestedFixes. -// We need to rethink the analyzer testing API to better support such -// cases. In the meantime, users of RunWithSuggestedFixes testing -// analyzers that offer alternative fixes are advised to put each fix -// in a separate .go file in the testdata. +// If a diagnostic offers multiple fixes for the same problem, they +// are almost certain to conflict, so in this case you should define +// the expected output using a multi-section txtar file as described +// above. func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result { results := Run(t, dir, a, patterns...) @@ -173,133 +169,165 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns for _, result := range results { act := result.Action - // file -> message -> edits - // TODO(adonovan): this mapping assumes fix.Messages are unique across analyzers, - // whereas they are only unique within a given Diagnostic. - fileEdits := make(map[*token.File]map[string][]diff.Edit) - - // We may assume that fixes are validated upon creation in Pass.Report. - // Group fixes by file and message. + // For each fix, split its edits by file and convert to diff form. + var ( + // fixEdits: message -> fixes -> filename -> edits + // + // TODO(adonovan): this mapping assumes fix.Messages + // are unique across analyzers, whereas they are only + // unique within a given Diagnostic. + fixEdits = make(map[string][]map[string][]diff.Edit) + allFilenames = make(map[string]bool) + ) for _, diag := range act.Diagnostics { + // Fixes are validated upon creation in Pass.Report. for _, fix := range diag.SuggestedFixes { // Assert that lazy fixes have a Category (#65578, #65087). if inTools && len(fix.TextEdits) == 0 && diag.Category == "" { t.Errorf("missing Diagnostic.Category for SuggestedFix without TextEdits (gopls requires the category for the name of the fix command") } + // Convert edits to diff form. + // Group fixes by message and file. + edits := make(map[string][]diff.Edit) for _, edit := range fix.TextEdits { file := act.Package.Fset.File(edit.Pos) - if _, ok := fileEdits[file]; !ok { - fileEdits[file] = make(map[string][]diff.Edit) - } - fileEdits[file][fix.Message] = append(fileEdits[file][fix.Message], diff.Edit{ + allFilenames[file.Name()] = true + edits[file.Name()] = append(edits[file.Name()], diff.Edit{ Start: file.Offset(edit.Pos), End: file.Offset(edit.End), New: string(edit.NewText), }) } + fixEdits[fix.Message] = append(fixEdits[fix.Message], edits) } } - for file, fixes := range fileEdits { - // Get the original file contents. - // TODO(adonovan): plumb pass.ReadFile. - orig, err := os.ReadFile(file.Name()) + merge := func(file, message string, x, y []diff.Edit) []diff.Edit { + z, ok := diff.Merge(x, y) + if !ok { + t.Errorf("in file %s, conflict applying fix %q", file, message) + return x // discard y + } + return z + } + + // Because the checking is driven by original + // filenames, there is no way to express that a fix + // (e.g. extract declaration) creates a new file. + for _, filename := range sortedKeys(allFilenames) { + // Read the original file. + content, err := os.ReadFile(filename) if err != nil { - t.Errorf("error reading %s: %v", file.Name(), err) + t.Errorf("error reading %s: %v", filename, err) continue } - // Get the golden file and read the contents. - ar, err := txtar.ParseFile(file.Name() + ".golden") + // check checks that the accumulated edits applied + // to the original content yield the wanted content. + check := func(prefix string, accumulated []diff.Edit, want []byte) { + if err := applyDiffsAndCompare(filename, content, want, accumulated); err != nil { + t.Errorf("%s: %s", prefix, err) + } + } + + // Read the golden file. It may have one of two forms: + // (1) A txtar archive with one section per fix title, + // including all fixes of just that title. + // (2) The expected output for file.Name after all (?) fixes are applied. + // This form requires that no diagnostic has multiple fixes. + ar, err := txtar.ParseFile(filename + ".golden") if err != nil { - t.Errorf("error reading %s.golden: %v", file.Name(), err) + t.Errorf("error reading %s.golden: %v", filename, err) continue } - if len(ar.Files) > 0 { - // one virtual file per kind of suggested fix - - if len(ar.Comment) != 0 { - // we allow either just the comment, or just virtual - // files, not both. it is not clear how "both" should - // behave. - t.Errorf("%s.golden has leading comment; we don't know what to do with it", file.Name()) + // Form #1: one archive section per kind of suggested fix. + if len(ar.Comment) > 0 { + // Disallow the combination of comment and archive sections. + t.Errorf("%s.golden has leading comment; we don't know what to do with it", filename) continue } - // Sort map keys for determinism in tests. - // TODO(jba): replace with slices.Sorted(maps.Keys(fixes)) when go.mod >= 1.23. - var keys []string - for k := range fixes { - keys = append(keys, k) - } - slices.Sort(keys) - for _, sf := range keys { - edits := fixes[sf] - found := false - for _, vf := range ar.Files { - if vf.Name == sf { - found = true - // the file may contain multiple trailing - // newlines if the user places empty lines - // between files in the archive. normalize - // this to a single newline. - golden := append(bytes.TrimRight(vf.Data, "\n"), '\n') - - if err := applyDiffsAndCompare(orig, golden, edits, file.Name()); err != nil { - t.Errorf("%s", err) - } - break - } - } - if !found { - t.Errorf("no section for suggested fix %q in %s.golden", sf, file.Name()) + + // Each archive section is named for a fix.Message. + // Accumulate the parts of the fix that apply to the current file, + // using a simple three-way merge, discarding conflicts, + // then apply the merged edits and compare to the archive section. + for _, section := range ar.Files { + message, want := section.Name, section.Data + var accumulated []diff.Edit + for _, fix := range fixEdits[message] { + accumulated = merge(filename, message, accumulated, fix[filename]) } - } - } else { - // all suggested fixes are represented by a single file - // TODO(adonovan): fix: this makes no sense if len(fixes) > 1. - // Sort map keys for determinism in tests. - // TODO(jba): replace with slices.Sorted(maps.Keys(fixes)) when go.mod >= 1.23. - var keys []string - for k := range fixes { - keys = append(keys, k) - } - slices.Sort(keys) - var catchallEdits []diff.Edit - for _, k := range keys { - catchallEdits = append(catchallEdits, fixes[k]...) + check(fmt.Sprintf("all fixes of message %q", message), accumulated, want) } - if err := applyDiffsAndCompare(orig, ar.Comment, catchallEdits, file.Name()); err != nil { - t.Errorf("%s", err) + } else { + // Form #2: all suggested fixes are represented by a single file. + want := ar.Comment + var accumulated []diff.Edit + for _, message := range sortedKeys(fixEdits) { + for _, fix := range fixEdits[message] { + accumulated = merge(filename, message, accumulated, fix[filename]) + } } + check("all fixes", accumulated, want) } } } + return results } -// applyDiffsAndCompare applies edits to src and compares the results against -// golden after formatting both. fileName is use solely for error reporting. -func applyDiffsAndCompare(src, golden []byte, edits []diff.Edit, fileName string) error { - out, err := diff.ApplyBytes(src, edits) +// applyDiffsAndCompare applies edits to original and compares the results against +// want after formatting both. fileName is use solely for error reporting. +func applyDiffsAndCompare(filename string, original, want []byte, edits []diff.Edit) error { + // Relativize filename, for tidier errors. + if cwd, err := os.Getwd(); err == nil { + if rel, err := filepath.Rel(cwd, filename); err == nil { + filename = rel + } + } + + if len(edits) == 0 { + return fmt.Errorf("%s: no edits", filename) + } + fixedBytes, err := diff.ApplyBytes(original, edits) if err != nil { - return fmt.Errorf("%s: error applying fixes: %v (see possible explanations at RunWithSuggestedFixes)", fileName, err) + return fmt.Errorf("%s: error applying fixes: %v (see possible explanations at RunWithSuggestedFixes)", filename, err) } - wantRaw, err := format.Source(golden) + fixed, err := format.Source(fixedBytes) if err != nil { - return fmt.Errorf("%s.golden: error formatting golden file: %v\n%s", fileName, err, out) + return fmt.Errorf("%s: error formatting resulting source: %v\n%s", filename, err, fixed) } - want := string(wantRaw) - formatted, err := format.Source(out) + want, err = format.Source(want) if err != nil { - return fmt.Errorf("%s: error formatting resulting source: %v\n%s", fileName, err, out) + return fmt.Errorf("%s.golden: error formatting golden file: %v\n%s", filename, err, fixed) + } + + // Keep error reporting logic below consistent with + // TestScript in ../internal/checker/fix_test.go! + + unified := func(xlabel, ylabel string, x, y []byte) string { + x = append(slices.Clip(bytes.TrimSpace(x)), '\n') + y = append(slices.Clip(bytes.TrimSpace(y)), '\n') + return diff.Unified(xlabel, ylabel, string(x), string(y)) } - if got := string(formatted); got != want { - unified := diff.Unified(fileName+".golden", "actual", want, got) - return fmt.Errorf("suggested fixes failed for %s:\n%s", fileName, unified) + + if diff := unified(filename+" (fixed)", filename+" (want)", fixed, want); diff != "" { + return fmt.Errorf("unexpected %s content:\n"+ + "-- original --\n%s\n"+ + "-- fixed --\n%s\n"+ + "-- want --\n%s\n"+ + "-- diff original fixed --\n%s\n"+ + "-- diff fixed want --\n%s", + filename, + original, + fixed, + want, + unified(filename+" (original)", filename+" (fixed)", original, fixed), + diff) } return nil } @@ -740,3 +768,13 @@ func sanitize(gopath, filename string) string { prefix := gopath + string(os.PathSeparator) + "src" + string(os.PathSeparator) return filepath.ToSlash(strings.TrimPrefix(filename, prefix)) } + +// TODO(adonovan): use better stuff from go1.23. +func sortedKeys[K cmp.Ordered, V any](m map[K]V) []K { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + slices.Sort(keys) + return keys +} diff --git a/go/analysis/internal/checker/fix_test.go b/go/analysis/internal/checker/fix_test.go index 8fb7506ac70..8f4e7a3f6a9 100644 --- a/go/analysis/internal/checker/fix_test.go +++ b/go/analysis/internal/checker/fix_test.go @@ -281,6 +281,9 @@ func TestScript(t *testing.T) { t.Logf("%s: $ %s\nstdout:\n%s\nstderr:\n%s", prefix, clean(cmd), stdout, lastStderr) } + // Keep error reporting logic below consistent with + // applyDiffsAndCompare in ../../analysistest/analysistest.go! + unified := func(xlabel, ylabel string, x, y []byte) string { x = append(slices.Clip(bytes.TrimSpace(x)), '\n') y = append(slices.Clip(bytes.TrimSpace(y)), '\n') diff --git a/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go.golden b/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go.golden index 9b2ba9a0b80..2d9447af3a3 100644 --- a/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/slicesdelete/slicesdelete.go.golden @@ -2,20 +2,6 @@ package slicesdelete import "slices" -import "slices" - -import "slices" - -import "slices" - -import "slices" - -import "slices" - -import "slices" - -import "slices" - var g struct{ f []int } func slicesdelete(test, other []byte, i int) { diff --git a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden index d97636fd311..34af5aad60b 100644 --- a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden @@ -2,8 +2,6 @@ package sortslice import "slices" -import "slices" - import "sort" type myint int From 0d16805d3d4f589b6910ab64a4c8e18dc5f02f16 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Tue, 11 Feb 2025 10:28:37 -0800 Subject: [PATCH 05/99] internal/stdlib: update stdlib index for Go 1.24.0 For golang/go#38706. Change-Id: Iaa6281ad4c18906c8dc1733df29ccc6b78130fb4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/648556 Reviewed-by: Cherry Mui Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Gopher Robot --- internal/stdlib/manifest.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/stdlib/manifest.go b/internal/stdlib/manifest.go index 9f0b871ff6b..e7d0aee2186 100644 --- a/internal/stdlib/manifest.go +++ b/internal/stdlib/manifest.go @@ -2151,6 +2151,8 @@ var PackageSymbols = map[string][]Symbol{ {"(Type).String", Method, 0}, {"(Version).GoString", Method, 0}, {"(Version).String", Method, 0}, + {"(VersionIndex).Index", Method, 24}, + {"(VersionIndex).IsHidden", Method, 24}, {"ARM_MAGIC_TRAMP_NUMBER", Const, 0}, {"COMPRESS_HIOS", Const, 6}, {"COMPRESS_HIPROC", Const, 6}, @@ -3834,6 +3836,7 @@ var PackageSymbols = map[string][]Symbol{ {"SymType", Type, 0}, {"SymVis", Type, 0}, {"Symbol", Type, 0}, + {"Symbol.HasVersion", Field, 24}, {"Symbol.Info", Field, 0}, {"Symbol.Library", Field, 13}, {"Symbol.Name", Field, 0}, @@ -3843,18 +3846,12 @@ var PackageSymbols = map[string][]Symbol{ {"Symbol.Value", Field, 0}, {"Symbol.Version", Field, 13}, {"Symbol.VersionIndex", Field, 24}, - {"Symbol.VersionScope", Field, 24}, - {"SymbolVersionScope", Type, 24}, {"Type", Type, 0}, {"VER_FLG_BASE", Const, 24}, {"VER_FLG_INFO", Const, 24}, {"VER_FLG_WEAK", Const, 24}, {"Version", Type, 0}, - {"VersionScopeGlobal", Const, 24}, - {"VersionScopeHidden", Const, 24}, - {"VersionScopeLocal", Const, 24}, - {"VersionScopeNone", Const, 24}, - {"VersionScopeSpecific", Const, 24}, + {"VersionIndex", Type, 24}, }, "debug/gosym": { {"(*DecodingError).Error", Method, 0}, From d2585c467c83030b6ee984e63dce55e799ff4741 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 31 Jan 2025 11:39:18 -0500 Subject: [PATCH 06/99] gopls/internal/golang: folding range: remove FoldingRangeInfo Another unnecessary data type. Updates golang/go#71489 Change-Id: I374f677afe44abf818a35741202579abfed4aeb3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/645855 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/golang/folding_range.go | 64 +++++++++++++------------- gopls/internal/server/folding_range.go | 21 +-------- 2 files changed, 34 insertions(+), 51 deletions(-) diff --git a/gopls/internal/golang/folding_range.go b/gopls/internal/golang/folding_range.go index 9d80cc8de29..4352da28151 100644 --- a/gopls/internal/golang/folding_range.go +++ b/gopls/internal/golang/folding_range.go @@ -6,6 +6,7 @@ package golang import ( "bytes" + "cmp" "context" "go/ast" "go/token" @@ -20,14 +21,8 @@ import ( "golang.org/x/tools/gopls/internal/util/safetoken" ) -// FoldingRangeInfo holds range and kind info of folding for an ast.Node -type FoldingRangeInfo struct { - Range protocol.Range - Kind protocol.FoldingRangeKind -} - // FoldingRange gets all of the folding range for f. -func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, lineFoldingOnly bool) (ranges []*FoldingRangeInfo, err error) { +func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, lineFoldingOnly bool) ([]protocol.FoldingRange, error) { // TODO(suzmue): consider limiting the number of folding ranges returned, and // implement a way to prioritize folding ranges in that case. pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) @@ -48,27 +43,29 @@ func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, } // Get folding ranges for comments separately as they are not walked by ast.Inspect. - ranges = append(ranges, commentsFoldingRange(pgf)...) + ranges := commentsFoldingRange(pgf) - visit := func(n ast.Node) bool { - rng := foldingRangeFunc(pgf, n, lineFoldingOnly) - if rng != nil { + // Walk the ast and collect folding ranges. + ast.Inspect(pgf.File, func(n ast.Node) bool { + if rng, ok := foldingRangeFunc(pgf, n, lineFoldingOnly); ok { ranges = append(ranges, rng) } return true - } - // Walk the ast and collect folding ranges. - ast.Inspect(pgf.File, visit) + }) - slices.SortFunc(ranges, func(x, y *FoldingRangeInfo) int { - return protocol.CompareRange(x.Range, y.Range) + // Sort by start position. + slices.SortFunc(ranges, func(x, y protocol.FoldingRange) int { + if d := cmp.Compare(x.StartLine, y.StartLine); d != 0 { + return d + } + return cmp.Compare(x.StartCharacter, y.StartCharacter) }) return ranges, nil } // foldingRangeFunc calculates the line folding range for ast.Node n -func foldingRangeFunc(pgf *parsego.File, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { +func foldingRangeFunc(pgf *parsego.File, n ast.Node, lineFoldingOnly bool) (protocol.FoldingRange, bool) { // TODO(suzmue): include trailing empty lines before the closing // parenthesis/brace. var kind protocol.FoldingRangeKind @@ -109,25 +106,22 @@ func foldingRangeFunc(pgf *parsego.File, n ast.Node, lineFoldingOnly bool) *Fold // Check that folding positions are valid. if !start.IsValid() || !end.IsValid() { - return nil + return protocol.FoldingRange{}, false } if start == end { // Nothing to fold. - return nil + return protocol.FoldingRange{}, false } // in line folding mode, do not fold if the start and end lines are the same. if lineFoldingOnly && safetoken.Line(pgf.Tok, start) == safetoken.Line(pgf.Tok, end) { - return nil + return protocol.FoldingRange{}, false } rng, err := pgf.PosRange(start, end) if err != nil { bug.Reportf("failed to create range: %s", err) // can't happen - return nil - } - return &FoldingRangeInfo{ - Range: rng, - Kind: kind, + return protocol.FoldingRange{}, false } + return foldingRange(kind, rng), true } // getLineFoldingRange returns the folding range for nodes with parentheses/braces/brackets @@ -196,7 +190,7 @@ func getLineFoldingRange(pgf *parsego.File, open, close token.Pos, lineFoldingOn // commentsFoldingRange returns the folding ranges for all comment blocks in file. // The folding range starts at the end of the first line of the comment block, and ends at the end of the // comment block and has kind protocol.Comment. -func commentsFoldingRange(pgf *parsego.File) (comments []*FoldingRangeInfo) { +func commentsFoldingRange(pgf *parsego.File) (comments []protocol.FoldingRange) { tokFile := pgf.Tok for _, commentGrp := range pgf.File.Comments { startGrpLine, endGrpLine := safetoken.Line(tokFile, commentGrp.Pos()), safetoken.Line(tokFile, commentGrp.End()) @@ -218,11 +212,19 @@ func commentsFoldingRange(pgf *parsego.File) (comments []*FoldingRangeInfo) { bug.Reportf("failed to create mapped range: %s", err) // can't happen continue } - comments = append(comments, &FoldingRangeInfo{ - // Fold from the end of the first line comment to the end of the comment block. - Range: rng, - Kind: protocol.Comment, - }) + // Fold from the end of the first line comment to the end of the comment block. + comments = append(comments, foldingRange(protocol.Comment, rng)) } return comments } + +func foldingRange(kind protocol.FoldingRangeKind, rng protocol.Range) protocol.FoldingRange { + return protocol.FoldingRange{ + // I have no idea why LSP doesn't use a protocol.Range here. + StartLine: rng.Start.Line, + StartCharacter: rng.Start.Character, + EndLine: rng.End.Line, + EndCharacter: rng.End.Character, + Kind: string(kind), + } +} diff --git a/gopls/internal/server/folding_range.go b/gopls/internal/server/folding_range.go index 95b2ffc0744..b05d5302f10 100644 --- a/gopls/internal/server/folding_range.go +++ b/gopls/internal/server/folding_range.go @@ -26,24 +26,5 @@ func (s *server) FoldingRange(ctx context.Context, params *protocol.FoldingRange if snapshot.FileKind(fh) != file.Go { return nil, nil // empty result } - ranges, err := golang.FoldingRange(ctx, snapshot, fh, snapshot.Options().LineFoldingOnly) - if err != nil { - return nil, err - } - return toProtocolFoldingRanges(ranges) -} - -func toProtocolFoldingRanges(ranges []*golang.FoldingRangeInfo) ([]protocol.FoldingRange, error) { - result := make([]protocol.FoldingRange, 0, len(ranges)) - for _, info := range ranges { - rng := info.Range - result = append(result, protocol.FoldingRange{ - StartLine: rng.Start.Line, - StartCharacter: rng.Start.Character, - EndLine: rng.End.Line, - EndCharacter: rng.End.Character, - Kind: string(info.Kind), - }) - } - return result, nil + return golang.FoldingRange(ctx, snapshot, fh, snapshot.Options().LineFoldingOnly) } From b3c5d108cdc6a215e5a4169c71a1c4dedbc69a83 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Tue, 11 Feb 2025 20:58:26 +0000 Subject: [PATCH 07/99] gopls: record telemetry counters for settings that are used Instrument telemetry recording which settings are passed to gopls. In some cases, this merely records whether settings were set. In others, it records buckets for the setting value. Fixes golang/go#71285 Change-Id: I820318fe9cf1b05accb3105e5e2d6ddc3c5e768f Reviewed-on: https://go-review.googlesource.com/c/tools/+/648416 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Auto-Submit: Robert Findley --- gopls/internal/cache/session_test.go | 3 +- gopls/internal/server/general.go | 17 +- gopls/internal/settings/settings.go | 201 ++++++++++++------- gopls/internal/settings/settings_test.go | 2 +- gopls/internal/telemetry/counterpath.go | 30 +++ gopls/internal/telemetry/counterpath_test.go | 47 +++++ gopls/internal/telemetry/telemetry_test.go | 44 ++++ 7 files changed, 262 insertions(+), 82 deletions(-) create mode 100644 gopls/internal/telemetry/counterpath.go create mode 100644 gopls/internal/telemetry/counterpath_test.go diff --git a/gopls/internal/cache/session_test.go b/gopls/internal/cache/session_test.go index 5f9a59a4945..1b7472af605 100644 --- a/gopls/internal/cache/session_test.go +++ b/gopls/internal/cache/session_test.go @@ -337,7 +337,8 @@ replace ( for _, f := range test.folders { opts := settings.DefaultOptions() if f.options != nil { - for _, err := range opts.Set(f.options(dir)) { + _, errs := opts.Set(f.options(dir)) + for _, err := range errs { t.Fatal(err) } } diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index 35614945f9d..de6b764c79f 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go @@ -28,6 +28,7 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/semtok" "golang.org/x/tools/gopls/internal/settings" + "golang.org/x/tools/gopls/internal/telemetry" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/goversion" "golang.org/x/tools/gopls/internal/util/moremaps" @@ -74,7 +75,11 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ // TODO(rfindley): eliminate this defer. defer func() { s.SetOptions(options) }() - s.handleOptionErrors(ctx, options.Set(params.InitializationOptions)) + // Process initialization options. + { + res, errs := options.Set(params.InitializationOptions) + s.handleOptionResult(ctx, res, errs) + } options.ForClientCapabilities(params.ClientInfo, params.Capabilities) if options.ShowBugReports { @@ -541,7 +546,8 @@ func (s *server) fetchFolderOptions(ctx context.Context, folder protocol.Documen opts = opts.Clone() for _, config := range configs { - s.handleOptionErrors(ctx, opts.Set(config)) + res, errs := opts.Set(config) + s.handleOptionResult(ctx, res, errs) } return opts, nil } @@ -555,7 +561,12 @@ func (s *server) eventuallyShowMessage(ctx context.Context, msg *protocol.ShowMe s.notifications = append(s.notifications, msg) } -func (s *server) handleOptionErrors(ctx context.Context, optionErrors []error) { +func (s *server) handleOptionResult(ctx context.Context, applied []telemetry.CounterPath, optionErrors []error) { + for _, path := range applied { + path = append(settings.CounterPath{"gopls", "setting"}, path...) + counter.Inc(path.FullName()) + } + var warnings, errs []string for _, err := range optionErrors { if err == nil { diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 8f33bdae96b..7d64cbef459 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/semtok" + "golang.org/x/tools/gopls/internal/telemetry" "golang.org/x/tools/gopls/internal/util/frob" ) @@ -822,10 +823,18 @@ const ( // TODO: support "Manual"? ) -// Set updates *options based on the provided JSON value: +type CounterPath = telemetry.CounterPath + +// Set updates *Options based on the provided JSON value: // null, bool, string, number, array, or object. +// +// The applied result describes settings that were applied. Each CounterPath +// contains at least the name of the setting, but may also include sub-setting +// names for settings that are themselves maps, and/or a non-empty bucket name +// when bucketing is desirable. +// // On failure, it returns one or more non-nil errors. -func (o *Options) Set(value any) (errors []error) { +func (o *Options) Set(value any) (applied []CounterPath, errs []error) { switch value := value.(type) { case nil: case map[string]any: @@ -840,19 +849,32 @@ func (o *Options) Set(value any) (errors []error) { name = split[len(split)-1] if _, ok := seen[name]; ok { - errors = append(errors, fmt.Errorf("duplicate value for %s", name)) + errs = append(errs, fmt.Errorf("duplicate value for %s", name)) } seen[name] = struct{}{} - if err := o.setOne(name, value); err != nil { + paths, err := o.setOne(name, value) + if err != nil { err := fmt.Errorf("setting option %q: %w", name, err) - errors = append(errors, err) + errs = append(errs, err) + } + _, soft := err.(*SoftError) + if err == nil || soft { + if len(paths) == 0 { + path := CounterPath{name, ""} + applied = append(applied, path) + } else { + for _, subpath := range paths { + path := append(CounterPath{name}, subpath...) + applied = append(applied, path) + } + } } } default: - errors = append(errors, fmt.Errorf("invalid options type %T (want JSON null or object)", value)) + errs = append(errs, fmt.Errorf("invalid options type %T (want JSON null or object)", value)) } - return errors + return applied, errs } func (o *Options) ForClientCapabilities(clientInfo *protocol.ClientInfo, caps protocol.ClientCapabilities) { @@ -955,14 +977,26 @@ func validateDirectoryFilter(ifilter string) (string, error) { } // setOne updates a field of o based on the name and value. +// +// The applied result describes the counter values to be updated as a result of +// the applied setting. If the result is nil, the default counter for this +// setting should be updated. +// +// For example, if the setting name is "foo", +// - If applied is nil, update the count for "foo". +// - If applied is []CounterPath{{"bucket"}}, update the count for +// foo:bucket. +// - If applied is []CounterPath{{"a","b"}, {"c","d"}}, update foo/a:b and +// foo/c:d. +// // It returns an error if the value was invalid or duplicate. // It is the caller's responsibility to augment the error with 'name'. -func (o *Options) setOne(name string, value any) error { +func (o *Options) setOne(name string, value any) (applied []CounterPath, _ error) { switch name { case "env": env, ok := value.(map[string]any) if !ok { - return fmt.Errorf("invalid type %T (want JSON object)", value) + return nil, fmt.Errorf("invalid type %T (want JSON object)", value) } if o.Env == nil { o.Env = make(map[string]string) @@ -973,30 +1007,32 @@ func (o *Options) setOne(name string, value any) error { case string, int: o.Env[k] = fmt.Sprint(v) default: - return fmt.Errorf("invalid map value %T (want string)", v) + return nil, fmt.Errorf("invalid map value %T (want string)", v) } } + return nil, nil case "buildFlags": - return setStringSlice(&o.BuildFlags, value) + return nil, setStringSlice(&o.BuildFlags, value) case "directoryFilters": filterStrings, err := asStringSlice(value) if err != nil { - return err + return nil, err } var filters []string for _, filterStr := range filterStrings { filter, err := validateDirectoryFilter(filterStr) if err != nil { - return err + return nil, err } filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/")) } o.DirectoryFilters = filters + return nil, nil case "workspaceFiles": - return setStringSlice(&o.WorkspaceFiles, value) + return nil, setStringSlice(&o.WorkspaceFiles, value) case "completionDocumentation": return setBool(&o.CompletionDocumentation, value) case "usePlaceholders": @@ -1006,7 +1042,7 @@ func (o *Options) setOne(name string, value any) error { case "completeUnimported": return setBool(&o.CompleteUnimported, value) case "completionBudget": - return setDuration(&o.CompletionBudget, value) + return nil, setDuration(&o.CompletionBudget, value) case "importsSource": return setEnum(&o.ImportsSource, value, ImportsSourceOff, @@ -1038,7 +1074,7 @@ func (o *Options) setOne(name string, value any) error { case "hoverKind": if s, ok := value.(string); ok && strings.EqualFold(s, "structured") { - return deprecatedError("the experimental hoverKind='structured' setting was removed in gopls/v0.18.0 (https://go.dev/issue/70233)") + return nil, deprecatedError("the experimental hoverKind='structured' setting was removed in gopls/v0.18.0 (https://go.dev/issue/70233)") } return setEnum(&o.HoverKind, value, NoDocumentation, @@ -1047,7 +1083,7 @@ func (o *Options) setOne(name string, value any) error { FullDocumentation) case "linkTarget": - return setString(&o.LinkTarget, value) + return nil, setString(&o.LinkTarget, value) case "linksInHover": switch value { @@ -1058,9 +1094,9 @@ func (o *Options) setOne(name string, value any) error { case "gopls": o.LinksInHover = LinksInHover_Gopls default: - return fmt.Errorf(`invalid value %s; expect false, true, or "gopls"`, - value) + return nil, fmt.Errorf(`invalid value %s; expect false, true, or "gopls"`, value) } + return nil, nil case "importShortcut": return setEnum(&o.ImportShortcut, value, @@ -1069,18 +1105,20 @@ func (o *Options) setOne(name string, value any) error { DefinitionShortcut) case "analyses": - if err := setBoolMap(&o.Analyses, value); err != nil { - return err + counts, err := setBoolMap(&o.Analyses, value) + if err != nil { + return nil, err } if o.Analyses["fieldalignment"] { - return deprecatedError("the 'fieldalignment' analyzer was removed in gopls/v0.17.0; instead, hover over struct fields to see size/offset information (https://go.dev/issue/66861)") + return counts, deprecatedError("the 'fieldalignment' analyzer was removed in gopls/v0.17.0; instead, hover over struct fields to see size/offset information (https://go.dev/issue/66861)") } + return counts, nil case "hints": return setBoolMap(&o.Hints, value) case "annotations": - return deprecatedError("the 'annotations' setting was removed in gopls/v0.18.0; all compiler optimization details are now shown") + return nil, deprecatedError("the 'annotations' setting was removed in gopls/v0.18.0; all compiler optimization details are now shown") case "vulncheck": return setEnum(&o.Vulncheck, value, @@ -1090,7 +1128,7 @@ func (o *Options) setOne(name string, value any) error { case "codelenses", "codelens": lensOverrides, err := asBoolMap[CodeLensSource](value) if err != nil { - return err + return nil, err } if o.Codelenses == nil { o.Codelenses = make(map[CodeLensSource]bool) @@ -1098,15 +1136,21 @@ func (o *Options) setOne(name string, value any) error { o.Codelenses = maps.Clone(o.Codelenses) maps.Copy(o.Codelenses, lensOverrides) + var counts []CounterPath + for k, v := range lensOverrides { + counts = append(counts, CounterPath{string(k), fmt.Sprint(v)}) + } + if name == "codelens" { - return deprecatedError("codelenses") + return counts, deprecatedError("codelenses") } + return counts, nil case "staticcheck": return setBool(&o.Staticcheck, value) case "local": - return setString(&o.Local, value) + return nil, setString(&o.Local, value) case "verboseOutput": return setBool(&o.VerboseOutput, value) @@ -1128,16 +1172,18 @@ func (o *Options) setOne(name string, value any) error { // TODO(hxjiang): deprecate noSemanticString and noSemanticNumber. case "noSemanticString": - if err := setBool(&o.NoSemanticString, value); err != nil { - return err + counts, err := setBool(&o.NoSemanticString, value) + if err != nil { + return nil, err } - return &SoftError{fmt.Sprintf("noSemanticString setting is deprecated, use semanticTokenTypes instead (though you can continue to apply them for the time being).")} + return counts, &SoftError{"noSemanticString setting is deprecated, use semanticTokenTypes instead (though you can continue to apply them for the time being)."} case "noSemanticNumber": - if err := setBool(&o.NoSemanticNumber, value); err != nil { - return nil + counts, err := setBool(&o.NoSemanticNumber, value) + if err != nil { + return nil, err } - return &SoftError{fmt.Sprintf("noSemanticNumber setting is deprecated, use semanticTokenTypes instead (though you can continue to apply them for the time being).")} + return counts, &SoftError{"noSemanticNumber setting is deprecated, use semanticTokenTypes instead (though you can continue to apply them for the time being)."} case "semanticTokenTypes": return setBoolMap(&o.SemanticTokenTypes, value) @@ -1157,15 +1203,16 @@ func (o *Options) setOne(name string, value any) error { case "templateExtensions": switch value := value.(type) { case []any: - return setStringSlice(&o.TemplateExtensions, value) + return nil, setStringSlice(&o.TemplateExtensions, value) case nil: o.TemplateExtensions = nil default: - return fmt.Errorf("unexpected type %T (want JSON array of string)", value) + return nil, fmt.Errorf("unexpected type %T (want JSON array of string)", value) } + return nil, nil case "diagnosticsDelay": - return setDuration(&o.DiagnosticsDelay, value) + return nil, setDuration(&o.DiagnosticsDelay, value) case "diagnosticsTrigger": return setEnum(&o.DiagnosticsTrigger, value, @@ -1175,11 +1222,8 @@ func (o *Options) setOne(name string, value any) error { case "analysisProgressReporting": return setBool(&o.AnalysisProgressReporting, value) - case "allowImplicitNetworkAccess": - return deprecatedError("") - case "standaloneTags": - return setStringSlice(&o.StandaloneTags, value) + return nil, setStringSlice(&o.StandaloneTags, value) case "subdirWatchPatterns": return setEnum(&o.SubdirWatchPatterns, value, @@ -1188,7 +1232,7 @@ func (o *Options) setOne(name string, value any) error { SubdirWatchPatternsAuto) case "reportAnalysisProgressAfter": - return setDuration(&o.ReportAnalysisProgressAfter, value) + return nil, setDuration(&o.ReportAnalysisProgressAfter, value) case "telemetryPrompt": return setBool(&o.TelemetryPrompt, value) @@ -1213,50 +1257,54 @@ func (o *Options) setOne(name string, value any) error { // renamed case "experimentalDisabledAnalyses": - return deprecatedError("analyses") + return nil, deprecatedError("analyses") case "disableDeepCompletion": - return deprecatedError("deepCompletion") + return nil, deprecatedError("deepCompletion") case "disableFuzzyMatching": - return deprecatedError("fuzzyMatching") + return nil, deprecatedError("fuzzyMatching") case "wantCompletionDocumentation": - return deprecatedError("completionDocumentation") + return nil, deprecatedError("completionDocumentation") case "wantUnimportedCompletions": - return deprecatedError("completeUnimported") + return nil, deprecatedError("completeUnimported") case "fuzzyMatching": - return deprecatedError("matcher") + return nil, deprecatedError("matcher") case "caseSensitiveCompletion": - return deprecatedError("matcher") + return nil, deprecatedError("matcher") case "experimentalDiagnosticsDelay": - return deprecatedError("diagnosticsDelay") + return nil, deprecatedError("diagnosticsDelay") // deprecated + + case "allowImplicitNetworkAccess": + return nil, deprecatedError("") + case "memoryMode": - return deprecatedError("") + return nil, deprecatedError("") case "tempModFile": - return deprecatedError("") + return nil, deprecatedError("") case "experimentalWorkspaceModule": - return deprecatedError("") + return nil, deprecatedError("") case "experimentalTemplateSupport": - return deprecatedError("") + return nil, deprecatedError("") case "experimentalWatchedFileDelay": - return deprecatedError("") + return nil, deprecatedError("") case "experimentalPackageCacheKey": - return deprecatedError("") + return nil, deprecatedError("") case "allowModfileModifications": - return deprecatedError("") + return nil, deprecatedError("") case "allExperiments": // golang/go#65548: this setting is a no-op, but we fail don't report it as @@ -1265,29 +1313,29 @@ func (o *Options) setOne(name string, value any) error { // If, in the future, VS Code stops injecting this, we could theoretically // report an error here, but it also seems harmless to keep ignoring this // setting forever. + return nil, nil case "experimentalUseInvalidMetadata": - return deprecatedError("") + return nil, deprecatedError("") case "newDiff": - return deprecatedError("") + return nil, deprecatedError("") case "wantSuggestedFixes": - return deprecatedError("") + return nil, deprecatedError("") case "noIncrementalSync": - return deprecatedError("") + return nil, deprecatedError("") case "watchFileChanges": - return deprecatedError("") + return nil, deprecatedError("") case "go-diff": - return deprecatedError("") + return nil, deprecatedError("") default: - return fmt.Errorf("unexpected setting") + return nil, fmt.Errorf("unexpected setting") } - return nil } // EnabledSemanticTokenModifiers returns a map of modifiers to boolean. @@ -1323,11 +1371,6 @@ func (e *SoftError) Error() string { return e.msg } -// softErrorf reports a soft error related to the current option. -func softErrorf(format string, args ...any) error { - return &SoftError{fmt.Sprintf(format, args...)} -} - // deprecatedError reports the current setting as deprecated. // The optional replacement is suggested to the user. func deprecatedError(replacement string) error { @@ -1341,13 +1384,13 @@ func deprecatedError(replacement string) error { // setT() and asT() helpers: the setT forms write to the 'dest *T' // variable only on success, to reduce boilerplate in Option.set. -func setBool(dest *bool, value any) error { +func setBool(dest *bool, value any) ([]CounterPath, error) { b, err := asBool(value) if err != nil { - return err + return nil, err } *dest = b - return nil + return []CounterPath{{fmt.Sprint(b)}}, nil } func asBool(value any) (bool, error) { @@ -1371,13 +1414,17 @@ func setDuration(dest *time.Duration, value any) error { return nil } -func setBoolMap[K ~string](dest *map[K]bool, value any) error { +func setBoolMap[K ~string](dest *map[K]bool, value any) ([]CounterPath, error) { m, err := asBoolMap[K](value) if err != nil { - return err + return nil, err } *dest = m - return nil + var counts []CounterPath + for k, v := range m { + counts = append(counts, CounterPath{string(k), fmt.Sprint(v)}) + } + return counts, nil } func asBoolMap[K ~string](value any) (map[K]bool, error) { @@ -1438,13 +1485,13 @@ func asStringSlice(value any) ([]string, error) { return slice, nil } -func setEnum[S ~string](dest *S, value any, options ...S) error { +func setEnum[S ~string](dest *S, value any, options ...S) ([]CounterPath, error) { enum, err := asEnum(value, options...) if err != nil { - return err + return nil, err } *dest = enum - return nil + return []CounterPath{{string(enum)}}, nil } func asEnum[S ~string](value any, options ...S) (S, error) { diff --git a/gopls/internal/settings/settings_test.go b/gopls/internal/settings/settings_test.go index 63b4aded8bd..05afa8ecac3 100644 --- a/gopls/internal/settings/settings_test.go +++ b/gopls/internal/settings/settings_test.go @@ -206,7 +206,7 @@ func TestOptions_Set(t *testing.T) { for _, test := range tests { var opts Options - err := opts.Set(map[string]any{test.name: test.value}) + _, err := opts.Set(map[string]any{test.name: test.value}) if err != nil { if !test.wantError { t.Errorf("Options.set(%q, %v) failed: %v", diff --git a/gopls/internal/telemetry/counterpath.go b/gopls/internal/telemetry/counterpath.go new file mode 100644 index 00000000000..e6d9d84b531 --- /dev/null +++ b/gopls/internal/telemetry/counterpath.go @@ -0,0 +1,30 @@ +// 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 telemetry + +import "strings" + +// A CounterPath represents the components of a telemetry counter name. +// +// By convention, counter names follow the format path/to/counter:bucket. The +// CounterPath holds the '/'-separated components of this path, along with a +// final element representing the bucket. +// +// CounterPaths may be used to build up counters incrementally, such as when a +// set of observed counters shared a common prefix, to be controlled by the +// caller. +type CounterPath []string + +// FullName returns the counter name for the receiver. +func (p CounterPath) FullName() string { + if len(p) == 0 { + return "" + } + name := strings.Join([]string(p[:len(p)-1]), "/") + if bucket := p[len(p)-1]; bucket != "" { + name += ":" + bucket + } + return name +} diff --git a/gopls/internal/telemetry/counterpath_test.go b/gopls/internal/telemetry/counterpath_test.go new file mode 100644 index 00000000000..b6ac7478b72 --- /dev/null +++ b/gopls/internal/telemetry/counterpath_test.go @@ -0,0 +1,47 @@ +// 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 telemetry + +import ( + "testing" +) + +// TestCounterPath tests the formatting of various counter paths. +func TestCounterPath(t *testing.T) { + tests := []struct { + path CounterPath + want string + }{ + { + path: CounterPath{}, + want: "", + }, + { + path: CounterPath{"counter"}, + want: ":counter", + }, + { + path: CounterPath{"counter", "bucket"}, + want: "counter:bucket", + }, + { + path: CounterPath{"path", "to", "counter"}, + want: "path/to:counter", + }, + { + path: CounterPath{"multi", "component", "path", "bucket"}, + want: "multi/component/path:bucket", + }, + { + path: CounterPath{"path", ""}, + want: "path", + }, + } + for _, tt := range tests { + if got := tt.path.FullName(); got != tt.want { + t.Errorf("CounterPath(%v).FullName() = %v, want %v", tt.path, got, tt.want) + } + } +} diff --git a/gopls/internal/telemetry/telemetry_test.go b/gopls/internal/telemetry/telemetry_test.go index 7aaca41ab55..4c41cc40dc9 100644 --- a/gopls/internal/telemetry/telemetry_test.go +++ b/gopls/internal/telemetry/telemetry_test.go @@ -119,6 +119,50 @@ func TestTelemetry(t *testing.T) { } } +func TestSettingTelemetry(t *testing.T) { + // counters that should be incremented by each session + sessionCounters := []*counter.Counter{ + counter.New("gopls/setting/diagnosticsDelay"), + counter.New("gopls/setting/staticcheck:true"), + counter.New("gopls/setting/noSemanticString:true"), + counter.New("gopls/setting/analyses/deprecated:false"), + } + + initialCounts := make([]uint64, len(sessionCounters)) + for i, c := range sessionCounters { + count, err := countertest.ReadCounter(c) + if err != nil { + continue // counter db not open, or counter not found + } + initialCounts[i] = count + } + + // Run gopls. + WithOptions( + Modes(Default), + Settings{ + "staticcheck": true, + "analyses": map[string]bool{ + "deprecated": false, + }, + "diagnosticsDelay": "0s", + "noSemanticString": true, + }, + ).Run(t, "", func(_ *testing.T, env *Env) { + }) + + for i, c := range sessionCounters { + count, err := countertest.ReadCounter(c) + if err != nil { + t.Errorf("ReadCounter(%q) failed: %v", c.Name(), err) + continue + } + if count <= initialCounts[i] { + t.Errorf("ReadCounter(%q) = %d, want > %d", c.Name(), count, initialCounts[i]) + } + } +} + func addForwardedCounters(env *Env, names []string, values []int64) { args, err := command.MarshalArgs(command.AddTelemetryCountersArgs{ Names: names, Values: values, From 25932623b63eed2010348abe2dc5ff3e1fe6f86d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 11 Feb 2025 16:04:17 -0500 Subject: [PATCH 08/99] gopls/internal/telemetry/cmd/stacks: remove leading \b match A directory separator / does not create word boundaries, so dir/file will not match \bfile. This CL removes the leading word-boundary match from the interpretation of string literals in stacks' claim expression language, which was causing spurious duplicate issues. + test Change-Id: Ie02be3591096ddf1d38b10873bed02449e35bd56 Reviewed-on: https://go-review.googlesource.com/c/tools/+/648579 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- gopls/internal/telemetry/cmd/stacks/stacks.go | 19 +++++++++++++++---- .../telemetry/cmd/stacks/stacks_test.go | 10 ++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/gopls/internal/telemetry/cmd/stacks/stacks.go b/gopls/internal/telemetry/cmd/stacks/stacks.go index 7cb20012657..36a675d0eb0 100644 --- a/gopls/internal/telemetry/cmd/stacks/stacks.go +++ b/gopls/internal/telemetry/cmd/stacks/stacks.go @@ -479,11 +479,20 @@ func parsePredicate(s string) (func(string) bool, error) { if err != nil { return err } - // The literal should match complete words. It may match multiple words, - // if it contains non-word runes like whitespace; but it must match word - // boundaries at each end. + // The end of the literal (usually "symbol", + // "pkg.symbol", or "pkg.symbol:+1") must + // match a word boundary. However, the start + // of the literal need not: an input line such + // as "domain.name/dir/pkg.symbol:+1" should + // match literal "pkg.symbol", but the slash + // is not a word boundary (witness: + // https://go.dev/play/p/w-8ev_VUBSq). + // + // It may match multiple words if it contains + // non-word runes like whitespace. + // // The constructed regular expression is always valid. - literalRegexps[e] = regexp.MustCompile(`\b` + regexp.QuoteMeta(lit) + `\b`) + literalRegexps[e] = regexp.MustCompile(regexp.QuoteMeta(lit) + `\b`) default: return fmt.Errorf("syntax error (%T)", e) @@ -1084,6 +1093,8 @@ type Issue struct { newStacks []string // new stacks to add to existing issue (comments and IDs) } +func (issue *Issue) String() string { return fmt.Sprintf("#%d", issue.Number) } + type User struct { Login string HTMLURL string `json:"html_url"` diff --git a/gopls/internal/telemetry/cmd/stacks/stacks_test.go b/gopls/internal/telemetry/cmd/stacks/stacks_test.go index 452113a1581..9f798aa43a3 100644 --- a/gopls/internal/telemetry/cmd/stacks/stacks_test.go +++ b/gopls/internal/telemetry/cmd/stacks/stacks_test.go @@ -85,13 +85,15 @@ func TestParsePredicate(t *testing.T) { want bool }{ {`"x"`, `"x"`, true}, - {`"x"`, `"axe"`, false}, // literals match whole words + {`"x"`, `"axe"`, false}, // literals must match word ends + {`"xe"`, `"axe"`, true}, {`"x"`, "val:x+5", true}, {`"fu+12"`, "x:fu+12,", true}, - {`"fu+12"`, "snafu+12,", false}, + {`"fu+12"`, "snafu+12,", true}, // literals needn't match word start {`"fu+12"`, "x:fu+123,", false}, - {`"a.*b"`, "a.*b", true}, // regexp metachars are escaped - {`"a.*b"`, "axxb", false}, // ditto + {`"foo:+12"`, "dir/foo:+12,", true}, // literals needn't match word start + {`"a.*b"`, "a.*b", true}, // regexp metachars are escaped + {`"a.*b"`, "axxb", false}, // ditto {`"x"`, `"y"`, false}, {`!"x"`, "x", false}, {`!"x"`, "y", true}, From d98774edc040d4c944774f1b6777522d4d921b54 Mon Sep 17 00:00:00 2001 From: Sean Liao Date: Sun, 9 Feb 2025 13:15:11 +0000 Subject: [PATCH 09/99] cmd/signature-fuzzer/internal/fuzz-generator: update to math/rand/v2 Fixes golang/go#71613 Change-Id: Id69044282568b3564aee82dfe4c1b98c41d16d0f Reviewed-on: https://go-review.googlesource.com/c/tools/+/647896 Reviewed-by: Than McIntosh Reviewed-by: Dmitri Shuralyov Auto-Submit: Dmitri Shuralyov Reviewed-by: Cherry Mui LUCI-TryBot-Result: Go LUCI --- .../internal/fuzz-generator/wraprand.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/signature-fuzzer/internal/fuzz-generator/wraprand.go b/cmd/signature-fuzzer/internal/fuzz-generator/wraprand.go index bba178dc317..f83a5f22e27 100644 --- a/cmd/signature-fuzzer/internal/fuzz-generator/wraprand.go +++ b/cmd/signature-fuzzer/internal/fuzz-generator/wraprand.go @@ -6,7 +6,7 @@ package generator import ( "fmt" - "math/rand" + "math/rand/v2" "os" "runtime" "strings" @@ -20,8 +20,7 @@ const ( ) func NewWrapRand(seed int64, ctl int) *wraprand { - rand.Seed(seed) - return &wraprand{seed: seed, ctl: ctl} + return &wraprand{seed: seed, ctl: ctl, rand: rand.New(rand.NewPCG(0, uint64(seed)))} } type wraprand struct { @@ -32,6 +31,7 @@ type wraprand struct { tag string calls []string ctl int + rand *rand.Rand } func (w *wraprand) captureCall(tag string, val string) { @@ -59,7 +59,7 @@ func (w *wraprand) captureCall(tag string, val string) { func (w *wraprand) Intn(n int64) int64 { w.intncalls++ - rv := rand.Int63n(n) + rv := w.rand.Int64N(n) if w.ctl&RandCtlCapture != 0 { w.captureCall("Intn", fmt.Sprintf("%d", rv)) } @@ -68,7 +68,7 @@ func (w *wraprand) Intn(n int64) int64 { func (w *wraprand) Float32() float32 { w.f32calls++ - rv := rand.Float32() + rv := w.rand.Float32() if w.ctl&RandCtlCapture != 0 { w.captureCall("Float32", fmt.Sprintf("%f", rv)) } @@ -77,7 +77,7 @@ func (w *wraprand) Float32() float32 { func (w *wraprand) NormFloat64() float64 { w.f64calls++ - rv := rand.NormFloat64() + rv := w.rand.NormFloat64() if w.ctl&RandCtlCapture != 0 { w.captureCall("NormFloat64", fmt.Sprintf("%f", rv)) } @@ -85,7 +85,7 @@ func (w *wraprand) NormFloat64() float64 { } func (w *wraprand) emitCalls(fn string) { - outf, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + outf, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666) if err != nil { panic(err) } From b752317a21a68f705b3f8845fb2696d6f977cf4e Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Tue, 11 Feb 2025 17:01:42 -0500 Subject: [PATCH 10/99] internal/analysisinternal: disable AddImport test without go command The AddImport test uses the default importer, which calls go list. This fails in environments that can't call the go command, like js/wasm. Add a predicate to testenv that asserts the need for the default importer, and call it from TestAddImport. A subtlety: although this bug manifested itself only for the dot-import cases, in fact all the test cases failed type checking on js/wasm for this reason. But a successful type-check is not a precondition for the test (see the new comment in TestAddImport). What caused the particular test case to fail was a bad diff resulting from how the edit was applied in the presence of that failure. Fixes golang/go#71645. Change-Id: Ib04afad108c323999bb67f329cf8d9cf329fead1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/648580 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- internal/analysisinternal/addimport_test.go | 6 ++++++ internal/testenv/testenv.go | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/internal/analysisinternal/addimport_test.go b/internal/analysisinternal/addimport_test.go index 12423b7c061..da7c7f90114 100644 --- a/internal/analysisinternal/addimport_test.go +++ b/internal/analysisinternal/addimport_test.go @@ -18,9 +18,12 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/tools/go/analysis" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/testenv" ) func TestAddImport(t *testing.T) { + testenv.NeedsDefaultImporter(t) + descr := func(s string) string { if _, _, line, ok := runtime.Caller(1); ok { return fmt.Sprintf("L%d %s", line, s) @@ -270,6 +273,9 @@ func _(io.Reader) { Implicits: make(map[ast.Node]types.Object), } conf := &types.Config{ + // We don't want to fail if there is an error during type checking: + // the error may be because we're missing an import, and adding imports + // is the whole point of AddImport. Error: func(err error) { t.Log(err) }, Importer: importer.Default(), } diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index 144f4f8fd64..5c541b7b19b 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -278,6 +278,16 @@ func NeedsGoBuild(t testing.TB) { NeedsTool(t, "go") } +// NeedsDefaultImporter skips t if the test uses the default importer, +// returned by [go/importer.Default]. +func NeedsDefaultImporter(t testing.TB) { + t.Helper() + // The default importer may call `go list` + // (in src/internal/exportdata/exportdata.go:lookupGorootExport), + // so check for the go tool. + NeedsTool(t, "go") +} + // ExitIfSmallMachine emits a helpful diagnostic and calls os.Exit(0) if the // current machine is a builder known to have scarce resources. // From b5a64bbcfd2247d59f40403a28ca8e3a9a417a24 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 7 Feb 2025 13:18:50 -0500 Subject: [PATCH 11/99] go/analysis/internal/checker: be silent with -fix Just apply the fixes without listing the diagnostics. Also: be verbose with -v. The -v flag exists for historical reasons to do with the vet CLI, but it had no effect. This change makes it effectively an alias for -debug=v, which no-one can remember how to spell. Also, list the number of fixes and fixes updated when -fix and -v. Change-Id: Ic2342ad4868b6c8649077bf13c48e8727dbeba37 Reviewed-on: https://go-review.googlesource.com/c/tools/+/647698 LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Reviewed-by: Robert Findley --- go/analysis/internal/checker/checker.go | 65 ++++++++++++++----- go/analysis/internal/checker/checker_test.go | 32 +++++---- go/analysis/internal/checker/fix_test.go | 4 +- go/analysis/internal/checker/start_test.go | 1 + .../internal/checker/testdata/diff.txt | 3 +- .../internal/checker/testdata/fixes.txt | 6 +- .../internal/checker/testdata/importdup.txt | 5 +- .../internal/checker/testdata/importdup2.txt | 5 +- .../internal/checker/testdata/noend.txt | 3 +- .../internal/checker/testdata/overlap.txt | 7 +- 10 files changed, 85 insertions(+), 46 deletions(-) diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index fb3c47b1625..2a9ff2931b3 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -86,7 +86,36 @@ func RegisterFlags() { // It provides most of the logic for the main functions of both the // singlechecker and the multi-analysis commands. // It returns the appropriate exit code. -func Run(args []string, analyzers []*analysis.Analyzer) int { +// +// TODO(adonovan): tests should not call this function directly; +// fiddling with global variables (flags) is error-prone and hostile +// to parallelism. Instead, use unit tests of the actual units (e.g. +// checker.Analyze) and integration tests (e.g. TestScript) of whole +// executables. +func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) { + // Instead of returning a code directly, + // call this function to monotonically increase the exit code. + // This allows us to keep going in the face of some errors + // without having to remember what code to return. + // + // TODO(adonovan): interpreting exit codes is like reading tea-leaves. + // Insted of wasting effort trying to encode a multidimensional result + // into 7 bits we should just emit structured JSON output, and + // an exit code of 0 or 1 for success or failure. + exitAtLeast := func(code int) { + exitcode = max(code, exitcode) + } + + // When analysisflags is linked in (for {single,multi}checker), + // then the -v flag is registered for complex legacy reasons + // related to cmd/vet CLI. + // Treat it as an undocumented alias for -debug=v. + if v := flag.CommandLine.Lookup("v"); v != nil && + v.Value.(flag.Getter).Get() == true && + !strings.Contains(Debug, "v") { + Debug += "v" + } + if CPUProfile != "" { f, err := os.Create(CPUProfile) if err != nil { @@ -142,17 +171,14 @@ func Run(args []string, analyzers []*analysis.Analyzer) int { initial, err := load(args, allSyntax) if err != nil { log.Print(err) - return 1 + exitAtLeast(1) + return } - // TODO(adonovan): simplify exit code logic by using a single - // exit code variable and applying "code = max(code, X)" each - // time an error of code X occurs. - pkgsExitCode := 0 // Print package and module errors regardless of RunDespiteErrors. // Do not exit if there are errors, yet. if n := packages.PrintErrors(initial); n > 0 { - pkgsExitCode = 1 + exitAtLeast(1) } var factLog io.Writer @@ -172,7 +198,8 @@ func Run(args []string, analyzers []*analysis.Analyzer) int { graph, err := checker.Analyze(analyzers, initial, opts) if err != nil { log.Print(err) - return 1 + exitAtLeast(1) + return } // Don't print the diagnostics, @@ -181,22 +208,22 @@ func Run(args []string, analyzers []*analysis.Analyzer) int { if err := applyFixes(graph.Roots, Diff); err != nil { // Fail when applying fixes failed. log.Print(err) - return 1 + exitAtLeast(1) + return } - // TODO(adonovan): don't proceed to print the text or JSON output - // if we applied fixes; stop here. - // - // return pkgsExitCode + // Don't proceed to print text/JSON, + // and don't report an error + // just because there were diagnostics. + return } // Print the results. If !RunDespiteErrors and there // are errors in the packages, this will have 0 exit // code. Otherwise, we prefer to return exit code // indicating diagnostics. - if diagExitCode := printDiagnostics(graph); diagExitCode != 0 { - return diagExitCode // there were diagnostics - } - return pkgsExitCode // package errors but no diagnostics + exitAtLeast(printDiagnostics(graph)) + + return } // printDiagnostics prints diagnostics in text or JSON form @@ -541,6 +568,10 @@ fixloop: } } + if dbg('v') { + log.Printf("applied %d fixes, updated %d files", len(fixes), filesUpdated) + } + return nil } diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go index fcf5f66e03e..9ec6e61cd73 100644 --- a/go/analysis/internal/checker/checker_test.go +++ b/go/analysis/internal/checker/checker_test.go @@ -49,8 +49,10 @@ func Foo() { t.Fatal(err) } path := filepath.Join(testdata, "src/rename/test.go") + checker.Fix = true checker.Run([]string{"file=" + path}, []*analysis.Analyzer{renameAnalyzer}) + checker.Fix = false contents, err := os.ReadFile(path) if err != nil { @@ -138,31 +140,33 @@ func NewT1() *T1 { return &T1{T} } // package from source. For the rest, it asks 'go list' for export data, // which fails because the compiler encounters the type error. Since the // errors come from 'go list', the driver doesn't run the analyzer. - {name: "despite-error", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noop}, code: 1}, + {name: "despite-error", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noop}, code: exitCodeFailed}, // The noopfact analyzer does use facts, so the driver loads source for // all dependencies, does type checking itself, recognizes the error as a // type error, and runs the analyzer. - {name: "despite-error-fact", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 1}, + {name: "despite-error-fact", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noopWithFact}, code: exitCodeFailed}, // combination of parse/type errors and no errors - {name: "despite-error-and-no-error", pattern: []string{rderrFile, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1}, + {name: "despite-error-and-no-error", pattern: []string{rderrFile, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: exitCodeFailed}, // non-existing package error - {name: "no-package", pattern: []string{"xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1}, - {name: "no-package-despite-error", pattern: []string{"abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1}, - {name: "no-multi-package-despite-error", pattern: []string{"xyz", "abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1}, + {name: "no-package", pattern: []string{"xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: exitCodeFailed}, + {name: "no-package-despite-error", pattern: []string{"abc"}, analyzers: []*analysis.Analyzer{noop}, code: exitCodeFailed}, + {name: "no-multi-package-despite-error", pattern: []string{"xyz", "abc"}, analyzers: []*analysis.Analyzer{noop}, code: exitCodeFailed}, // combination of type/parsing and different errors - {name: "different-errors", pattern: []string{rderrFile, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1}, + {name: "different-errors", pattern: []string{rderrFile, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: exitCodeFailed}, // non existing dir error - {name: "no-match-dir", pattern: []string{"file=non/existing/dir"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1}, + {name: "no-match-dir", pattern: []string{"file=non/existing/dir"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: exitCodeFailed}, // no errors - {name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 0}, + {name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: exitCodeSuccess}, // duplicate list error with no findings - {name: "list-error", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{noop}, code: 1}, + {name: "list-error", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{noop}, code: exitCodeFailed}, // duplicate list errors with findings (issue #67790) - {name: "list-error-findings", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 3}, + {name: "list-error-findings", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: exitCodeDiagnostics}, } { - if got := checker.Run(test.pattern, test.analyzers); got != test.code { - t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code) - } + t.Run(test.name, func(t *testing.T) { + if got := checker.Run(test.pattern, test.analyzers); got != test.code { + t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code) + } + }) } } diff --git a/go/analysis/internal/checker/fix_test.go b/go/analysis/internal/checker/fix_test.go index 8f4e7a3f6a9..68d965d08d6 100644 --- a/go/analysis/internal/checker/fix_test.go +++ b/go/analysis/internal/checker/fix_test.go @@ -52,9 +52,9 @@ func TestMain(m *testing.M) { } const ( - exitCodeSuccess = 0 // success (no diagnostics) + exitCodeSuccess = 0 // success (no diagnostics, or successful -fix) exitCodeFailed = 1 // analysis failed to run - exitCodeDiagnostics = 3 // diagnostics were reported + exitCodeDiagnostics = 3 // diagnostics were reported (and no -fix) ) // TestReportInvalidDiagnostic tests that a call to pass.Report with diff --git a/go/analysis/internal/checker/start_test.go b/go/analysis/internal/checker/start_test.go index 618ccd09b93..c78829a5adf 100644 --- a/go/analysis/internal/checker/start_test.go +++ b/go/analysis/internal/checker/start_test.go @@ -40,6 +40,7 @@ package comment path := filepath.Join(testdata, "src/comment/doc.go") checker.Fix = true checker.Run([]string{"file=" + path}, []*analysis.Analyzer{commentAnalyzer}) + checker.Fix = false contents, err := os.ReadFile(path) if err != nil { diff --git a/go/analysis/internal/checker/testdata/diff.txt b/go/analysis/internal/checker/testdata/diff.txt index 5a0c9c2a3b2..f11f01ad1e4 100644 --- a/go/analysis/internal/checker/testdata/diff.txt +++ b/go/analysis/internal/checker/testdata/diff.txt @@ -8,8 +8,7 @@ skip GOOS=windows checker -rename -fix -diff example.com/p -exit 3 -stderr renaming "bar" to "baz" +exit 0 -- go.mod -- module example.com diff --git a/go/analysis/internal/checker/testdata/fixes.txt b/go/analysis/internal/checker/testdata/fixes.txt index 89f245f9ace..4d906ca3f54 100644 --- a/go/analysis/internal/checker/testdata/fixes.txt +++ b/go/analysis/internal/checker/testdata/fixes.txt @@ -2,9 +2,9 @@ # particular when processing duplicate fixes for overlapping packages # in the same directory ("p", "p [p.test]", "p_test [p.test]"). -checker -rename -fix example.com/p -exit 3 -stderr renaming "bar" to "baz" +checker -rename -fix -v example.com/p +stderr applied 8 fixes, updated 3 files +exit 0 -- go.mod -- module example.com diff --git a/go/analysis/internal/checker/testdata/importdup.txt b/go/analysis/internal/checker/testdata/importdup.txt index e1783777858..4c144a61221 100644 --- a/go/analysis/internal/checker/testdata/importdup.txt +++ b/go/analysis/internal/checker/testdata/importdup.txt @@ -1,8 +1,9 @@ # Test that duplicate imports--and, more generally, duplicate # identical insertions--are coalesced. -checker -marker -fix example.com/a -exit 3 +checker -marker -fix -v example.com/a +stderr applied 2 fixes, updated 1 files +exit 0 -- go.mod -- module example.com diff --git a/go/analysis/internal/checker/testdata/importdup2.txt b/go/analysis/internal/checker/testdata/importdup2.txt index 118fdc0184b..c2da0f33195 100644 --- a/go/analysis/internal/checker/testdata/importdup2.txt +++ b/go/analysis/internal/checker/testdata/importdup2.txt @@ -19,8 +19,9 @@ # In more complex examples, the result # may be more subtly order-dependent. -checker -marker -fix example.com/a example.com/b -exit 3 +checker -marker -fix -v example.com/a example.com/b +stderr applied 6 fixes, updated 2 files +exit 0 -- go.mod -- module example.com diff --git a/go/analysis/internal/checker/testdata/noend.txt b/go/analysis/internal/checker/testdata/noend.txt index 2d6be074565..5ebc5e011ba 100644 --- a/go/analysis/internal/checker/testdata/noend.txt +++ b/go/analysis/internal/checker/testdata/noend.txt @@ -2,8 +2,7 @@ # interpreted as if equal to SuggestedFix.Pos (see issue #64199). checker -noend -fix example.com/a -exit 3 -stderr say hello +exit 0 -- go.mod -- module example.com diff --git a/go/analysis/internal/checker/testdata/overlap.txt b/go/analysis/internal/checker/testdata/overlap.txt index f556ef308b9..581f2e18950 100644 --- a/go/analysis/internal/checker/testdata/overlap.txt +++ b/go/analysis/internal/checker/testdata/overlap.txt @@ -15,9 +15,12 @@ # (This is a pretty unlikely situation, but it corresponds # to a historical test, TestOther, that used to check for # a conflict, and it seemed wrong to delete it without explanation.) +# +# The fixes are silently and successfully applied. -checker -rename -marker -fix example.com/a -exit 3 +checker -rename -marker -fix -v example.com/a +stderr applied 2 fixes, updated 1 file +exit 0 -- go.mod -- module example.com From f9aad7054b5ff7461d687469b3329b583093e72e Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Wed, 12 Feb 2025 10:43:03 -0500 Subject: [PATCH 12/99] go/types/typeutil: avoid shifting uintptr by 32 on 32-bit archs Shifting by 32 on an uintptr causes vet's check for shifts that equal or exceed the width of the integer to trigger on 32-bit architectures. For example, on 386: $ GOARCH=386 GOOS=linux go vet golang.org/x/tools/go/types/typeutil # golang.org/x/tools/go/types/typeutil go/types/typeutil/map.go:393:24: hash (32 bits) too small for shift of 32 Because this package is vendored into the main Go tree, and go test has a special case to turn on all vet checks there, the finding is promoted to an error (even if the code is otherwise harmless). Fix it. For golang/go#69407. For golang/go#36905. Change-Id: Ib8bf981bbe338db4ba8e9b7add0b373acae7338d Cq-Include-Trybots: luci.golang.try:x_tools-gotip-linux-386-longtest,x_tools-gotip-linux-amd64-longtest Reviewed-on: https://go-review.googlesource.com/c/tools/+/648895 LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov Auto-Submit: Dmitri Shuralyov Reviewed-by: Alan Donovan --- go/types/typeutil/map.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/go/types/typeutil/map.go b/go/types/typeutil/map.go index 43261147c05..b6d542c64ee 100644 --- a/go/types/typeutil/map.go +++ b/go/types/typeutil/map.go @@ -389,8 +389,13 @@ func (hasher) hashTypeName(tname *types.TypeName) uint32 { // path, and whether or not it is a package-level typename. It // is rare for a package to define multiple local types with // the same name.) - hash := uintptr(unsafe.Pointer(tname)) - return uint32(hash ^ (hash >> 32)) + ptr := uintptr(unsafe.Pointer(tname)) + if unsafe.Sizeof(ptr) == 8 { + hash := uint64(ptr) + return uint32(hash ^ (hash >> 32)) + } else { + return uint32(ptr) + } } // shallowHash computes a hash of t without looking at any of its From 57629448da517f7c9b04e039d70bf2aa06d02ee6 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Tue, 11 Feb 2025 18:49:54 -0500 Subject: [PATCH 13/99] gopls/internal/analysis/gofix: check package visibility If the RHS of an inlinable constant is in a package that is not visible from the current package, do not report that it can be inlined. For golang/go#32816. Change-Id: Iff9e18f844aa898beb9f1f8df01142057b341c39 Reviewed-on: https://go-review.googlesource.com/c/tools/+/648581 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/gofix/gofix.go | 3 +++ gopls/internal/analysis/gofix/testdata/src/a/a.go | 5 +++++ gopls/internal/analysis/gofix/testdata/src/a/a.go.golden | 5 +++++ gopls/internal/analysis/gofix/testdata/src/a/internal/d.go | 5 +++++ gopls/internal/analysis/gofix/testdata/src/b/b.go | 2 ++ gopls/internal/analysis/gofix/testdata/src/b/b.go.golden | 2 ++ 6 files changed, 22 insertions(+) create mode 100644 gopls/internal/analysis/gofix/testdata/src/a/internal/d.go diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index 101924366d6..8460286bbe3 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -261,6 +261,9 @@ func run(pass *analysis.Pass) (any, error) { // "B" means something different here than at the inlinable const's scope. continue } + } else if !analysisinternal.CanImport(pass.Pkg.Path(), fcon.RHSPkgPath) { + // If this package can't see the RHS's package, we can't inline. + continue } var ( importPrefix string diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go b/gopls/internal/analysis/gofix/testdata/src/a/a.go index ae486746e5b..4f41b9a8c5d 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/a.go +++ b/gopls/internal/analysis/gofix/testdata/src/a/a.go @@ -1,5 +1,7 @@ package a +import "a/internal" + // Functions. func f() { @@ -75,6 +77,9 @@ const ( in8 = x ) +//go:fix inline +const D = internal.D // want D: `goFixInline const "a/internal".D` + func shadow() { var x int // shadows x at package scope diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden index 7d75a598fb7..9e9cc25996f 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden +++ b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden @@ -1,5 +1,7 @@ package a +import "a/internal" + // Functions. func f() { @@ -75,6 +77,9 @@ const ( in8 = x ) +//go:fix inline +const D = internal.D // want D: `goFixInline const "a/internal".D` + func shadow() { var x int // shadows x at package scope diff --git a/gopls/internal/analysis/gofix/testdata/src/a/internal/d.go b/gopls/internal/analysis/gofix/testdata/src/a/internal/d.go new file mode 100644 index 00000000000..3211d7ae3cc --- /dev/null +++ b/gopls/internal/analysis/gofix/testdata/src/a/internal/d.go @@ -0,0 +1,5 @@ +// According to the go toolchain's rule about internal packages, +// this package is visible to package a, but not package b. +package internal + +const D = 1 diff --git a/gopls/internal/analysis/gofix/testdata/src/b/b.go b/gopls/internal/analysis/gofix/testdata/src/b/b.go index 4bf9f0dc650..74876738bea 100644 --- a/gopls/internal/analysis/gofix/testdata/src/b/b.go +++ b/gopls/internal/analysis/gofix/testdata/src/b/b.go @@ -28,3 +28,5 @@ func g() { _ = a _ = x } + +const d = a.D // nope: a.D refers to a constant in a package that is not visible here. diff --git a/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden b/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden index b26a05c3046..b3608d6793e 100644 --- a/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden +++ b/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden @@ -32,3 +32,5 @@ func g() { _ = a _ = x } + +const d = a.D // nope: a.D refers to a constant in a package that is not visible here. From 86f13a91fb506bb6aee3a8a398f8a639c5212425 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Tue, 11 Feb 2025 18:58:26 -0500 Subject: [PATCH 14/99] gopls/internal/analysis/gofix: rename local Trivial: rename a local from fcon (forwardable const) to incon (inlinable const) to match terminology. For golang/go#32816. Change-Id: I7d61f055c7057c30b240c076b8710f47f2bf86d1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/648715 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/gofix/gofix.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index 8460286bbe3..8ec31bd4736 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -220,15 +220,15 @@ func run(pass *analysis.Pass) (any, error) { case *ast.Ident: // If the identifier is a use of an inlinable constant, suggest inlining it. if con, ok := pass.TypesInfo.Uses[n].(*types.Const); ok { - fcon, ok := inlinableConsts[con] + incon, ok := inlinableConsts[con] if !ok { var fact goFixInlineConstFact if pass.ImportObjectFact(con, &fact) { - fcon = &fact - inlinableConsts[con] = fcon + incon = &fact + inlinableConsts[con] = incon } } - if fcon == nil { + if incon == nil { continue // nope } @@ -248,20 +248,20 @@ func run(pass *analysis.Pass) (any, error) { // If the RHS is not in the current package, AddImport will handle // shadowing, so we only need to worry about when both expressions // are in the current package. - if pass.Pkg.Path() == fcon.RHSPkgPath { + if pass.Pkg.Path() == incon.RHSPkgPath { // fcon.rhsObj is the object referred to by B in the definition of A. scope := pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope - _, obj := scope.LookupParent(fcon.RHSName, n.Pos()) // what "B" means in n's scope + _, obj := scope.LookupParent(incon.RHSName, n.Pos()) // what "B" means in n's scope if obj == nil { // Should be impossible: if code at n can refer to the LHS, // it can refer to the RHS. - panic(fmt.Sprintf("no object for inlinable const %s RHS %s", n.Name, fcon.RHSName)) + panic(fmt.Sprintf("no object for inlinable const %s RHS %s", n.Name, incon.RHSName)) } - if obj != fcon.rhsObj { + if obj != incon.rhsObj { // "B" means something different here than at the inlinable const's scope. continue } - } else if !analysisinternal.CanImport(pass.Pkg.Path(), fcon.RHSPkgPath) { + } else if !analysisinternal.CanImport(pass.Pkg.Path(), incon.RHSPkgPath) { // If this package can't see the RHS's package, we can't inline. continue } @@ -269,9 +269,9 @@ func run(pass *analysis.Pass) (any, error) { importPrefix string edits []analysis.TextEdit ) - if fcon.RHSPkgPath != pass.Pkg.Path() { + if incon.RHSPkgPath != pass.Pkg.Path() { _, importPrefix, edits = analysisinternal.AddImport( - pass.TypesInfo, curFile, fcon.RHSPkgName, fcon.RHSPkgPath, fcon.RHSName, n.Pos()) + pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos()) } var ( pos = n.Pos() @@ -287,7 +287,7 @@ func run(pass *analysis.Pass) (any, error) { edits = append(edits, analysis.TextEdit{ Pos: pos, End: end, - NewText: []byte(importPrefix + fcon.RHSName), + NewText: []byte(importPrefix + incon.RHSName), }) pass.Report(analysis.Diagnostic{ Pos: pos, From 2f1b076c4ab654a507fef278b9a1d4a6bae56f04 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 12 Feb 2025 17:24:58 -0500 Subject: [PATCH 15/99] x/tools: add //go:fix inline This CL adds //go:fix inline annotations to some deprecated functions that may be inlined. Updates golang/go#32816 Change-Id: I2e8e82bee054721f266506af24ea39cf2e8b7983 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649056 LUCI-TryBot-Result: Go LUCI Commit-Queue: Alan Donovan Auto-Submit: Alan Donovan Reviewed-by: Jonathan Amsterdam --- go/ast/astutil/util.go | 2 ++ go/packages/packages.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/go/ast/astutil/util.go b/go/ast/astutil/util.go index ca71e3e1055..c820b208499 100644 --- a/go/ast/astutil/util.go +++ b/go/ast/astutil/util.go @@ -8,4 +8,6 @@ import "go/ast" // Unparen returns e with any enclosing parentheses stripped. // Deprecated: use [ast.Unparen]. +// +//go:fix inline func Unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } diff --git a/go/packages/packages.go b/go/packages/packages.go index c3a59b8ebf4..342f019a0f9 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -141,6 +141,8 @@ const ( LoadAllSyntax = LoadSyntax | NeedDeps // Deprecated: NeedExportsFile is a historical misspelling of NeedExportFile. + // + //go:fix inline NeedExportsFile = NeedExportFile ) From d0d86e40a80dcab58f5cd2fa5f81e650d0777817 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 12 Feb 2025 17:26:02 -0500 Subject: [PATCH 16/99] x/tools: run gopls/internal/analysis/gofix/main.go -fix This inlines calls to a number of deprecated functions in both std and x/tools. Updates golang/go#32816 Change-Id: Id7f89983b1428fd3c042947dbecf07349f0bc134 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649057 LUCI-TryBot-Result: Go LUCI Commit-Queue: Alan Donovan Auto-Submit: Alan Donovan Reviewed-by: Jonathan Amsterdam --- go/analysis/checker/checker.go | 2 +- go/analysis/passes/copylock/copylock.go | 2 +- go/analysis/passes/unusedresult/unusedresult.go | 4 +--- go/analysis/validate.go | 2 +- go/ast/astutil/rewrite.go | 2 +- go/callgraph/vta/propagation_test.go | 2 +- go/internal/gccgoimporter/parser.go | 4 ++-- go/ssa/interp/reflect.go | 4 ++-- go/ssa/util.go | 2 +- go/ssa/wrappers.go | 6 ++---- go/types/objectpath/objectpath_test.go | 2 +- internal/astutil/clone.go | 2 +- internal/gcimporter/ureader_yes.go | 2 +- internal/refactor/inline/inline.go | 2 +- internal/refactor/inline/inline_test.go | 2 +- internal/tool/tool.go | 4 ++-- refactor/eg/match.go | 4 +--- refactor/eg/rewrite.go | 2 +- refactor/rename/util.go | 4 +--- 19 files changed, 23 insertions(+), 31 deletions(-) diff --git a/go/analysis/checker/checker.go b/go/analysis/checker/checker.go index 502ec922179..94808733b9d 100644 --- a/go/analysis/checker/checker.go +++ b/go/analysis/checker/checker.go @@ -594,7 +594,7 @@ func (act *Action) exportPackageFact(fact analysis.Fact) { func factType(fact analysis.Fact) reflect.Type { t := reflect.TypeOf(fact) - if t.Kind() != reflect.Ptr { + if t.Kind() != reflect.Pointer { log.Fatalf("invalid Fact type: got %T, want pointer", fact) } return t diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go index a9f02ac62e6..8a215677165 100644 --- a/go/analysis/passes/copylock/copylock.go +++ b/go/analysis/passes/copylock/copylock.go @@ -378,7 +378,7 @@ var lockerType *types.Interface // Construct a sync.Locker interface type. func init() { - nullary := types.NewSignature(nil, nil, nil, false) // func() + nullary := types.NewSignatureType(nil, nil, nil, nil, nil, false) // func() methods := []*types.Func{ types.NewFunc(token.NoPos, nil, "Lock", nullary), types.NewFunc(token.NoPos, nil, "Unlock", nullary), diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go index d7cc1e6ae2c..e298f644277 100644 --- a/go/analysis/passes/unusedresult/unusedresult.go +++ b/go/analysis/passes/unusedresult/unusedresult.go @@ -130,9 +130,7 @@ func run(pass *analysis.Pass) (interface{}, error) { } // func() string -var sigNoArgsStringResult = types.NewSignature(nil, nil, - types.NewTuple(types.NewParam(token.NoPos, nil, "", types.Typ[types.String])), - false) +var sigNoArgsStringResult = types.NewSignatureType(nil, nil, nil, nil, types.NewTuple(types.NewParam(token.NoPos, nil, "", types.Typ[types.String])), false) type stringSetFlag map[string]bool diff --git a/go/analysis/validate.go b/go/analysis/validate.go index 4f2c4045622..14539392116 100644 --- a/go/analysis/validate.go +++ b/go/analysis/validate.go @@ -63,7 +63,7 @@ func Validate(analyzers []*Analyzer) error { return fmt.Errorf("fact type %s registered by two analyzers: %v, %v", t, a, prev) } - if t.Kind() != reflect.Ptr { + if t.Kind() != reflect.Pointer { return fmt.Errorf("%s: fact type %s is not a pointer", a, t) } factTypes[t] = a diff --git a/go/ast/astutil/rewrite.go b/go/ast/astutil/rewrite.go index 58934f76633..5c8dbbb7a35 100644 --- a/go/ast/astutil/rewrite.go +++ b/go/ast/astutil/rewrite.go @@ -183,7 +183,7 @@ type application struct { func (a *application) apply(parent ast.Node, name string, iter *iterator, n ast.Node) { // convert typed nil into untyped nil - if v := reflect.ValueOf(n); v.Kind() == reflect.Ptr && v.IsNil() { + if v := reflect.ValueOf(n); v.Kind() == reflect.Pointer && v.IsNil() { n = nil } diff --git a/go/callgraph/vta/propagation_test.go b/go/callgraph/vta/propagation_test.go index 492258f81e3..3885ef201cb 100644 --- a/go/callgraph/vta/propagation_test.go +++ b/go/callgraph/vta/propagation_test.go @@ -203,7 +203,7 @@ func testSuite() map[string]*vtaGraph { a := newNamedType("A") b := newNamedType("B") c := newNamedType("C") - sig := types.NewSignature(nil, types.NewTuple(), types.NewTuple(), false) + sig := types.NewSignatureType(nil, nil, nil, types.NewTuple(), types.NewTuple(), false) f1 := &ssa.Function{Signature: sig} setName(f1, "F1") diff --git a/go/internal/gccgoimporter/parser.go b/go/internal/gccgoimporter/parser.go index f315ec41004..f70946edbe4 100644 --- a/go/internal/gccgoimporter/parser.go +++ b/go/internal/gccgoimporter/parser.go @@ -619,7 +619,7 @@ func (p *parser) parseNamedType(nlist []interface{}) types.Type { p.skipInlineBody() p.expectEOL() - sig := types.NewSignature(receiver, params, results, isVariadic) + sig := types.NewSignatureType(receiver, nil, nil, params, results, isVariadic) nt.AddMethod(types.NewFunc(token.NoPos, pkg, name, sig)) } } @@ -800,7 +800,7 @@ func (p *parser) parseFunctionType(pkg *types.Package, nlist []interface{}) *typ params, isVariadic := p.parseParamList(pkg) results := p.parseResultList(pkg) - *t = *types.NewSignature(nil, params, results, isVariadic) + *t = *types.NewSignatureType(nil, nil, nil, params, results, isVariadic) return t } diff --git a/go/ssa/interp/reflect.go b/go/ssa/interp/reflect.go index 8259e56d860..22f8cde89c0 100644 --- a/go/ssa/interp/reflect.go +++ b/go/ssa/interp/reflect.go @@ -231,7 +231,7 @@ func reflectKind(t types.Type) reflect.Kind { case *types.Map: return reflect.Map case *types.Pointer: - return reflect.Ptr + return reflect.Pointer case *types.Slice: return reflect.Slice case *types.Struct: @@ -510,7 +510,7 @@ func newMethod(pkg *ssa.Package, recvType types.Type, name string) *ssa.Function // that is needed is the "pointerness" of Recv.Type, and for // now, we'll set it to always be false since we're only // concerned with rtype. Encapsulate this better. - sig := types.NewSignature(types.NewParam(token.NoPos, nil, "recv", recvType), nil, nil, false) + sig := types.NewSignatureType(types.NewParam(token.NoPos, nil, "recv", recvType), nil, nil, nil, nil, false) fn := pkg.Prog.NewFunction(name, sig, "fake reflect method") fn.Pkg = pkg return fn diff --git a/go/ssa/util.go b/go/ssa/util.go index 4a056cbe0bd..56638129602 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -195,7 +195,7 @@ func makeLen(T types.Type) *Builtin { lenParams := types.NewTuple(anonVar(T)) return &Builtin{ name: "len", - sig: types.NewSignature(nil, lenParams, lenResults, false), + sig: types.NewSignatureType(nil, nil, nil, lenParams, lenResults, false), } } diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index d09b4f250ee..aeb160eff23 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go @@ -106,9 +106,7 @@ func (b *builder) buildWrapper(fn *Function) { var c Call c.Call.Value = &Builtin{ name: "ssa:wrapnilchk", - sig: types.NewSignature(nil, - types.NewTuple(anonVar(fn.method.recv), anonVar(tString), anonVar(tString)), - types.NewTuple(anonVar(fn.method.recv)), false), + sig: types.NewSignatureType(nil, nil, nil, types.NewTuple(anonVar(fn.method.recv), anonVar(tString), anonVar(tString)), types.NewTuple(anonVar(fn.method.recv)), false), } c.Call.Args = []Value{ v, @@ -262,7 +260,7 @@ func createThunk(prog *Program, sel *selection) *Function { } func changeRecv(s *types.Signature, recv *types.Var) *types.Signature { - return types.NewSignature(recv, s.Params(), s.Results(), s.Variadic()) + return types.NewSignatureType(recv, nil, nil, s.Params(), s.Results(), s.Variadic()) } // A local version of *types.Selection. diff --git a/go/types/objectpath/objectpath_test.go b/go/types/objectpath/objectpath_test.go index 0805c9d919a..642d6da4926 100644 --- a/go/types/objectpath/objectpath_test.go +++ b/go/types/objectpath/objectpath_test.go @@ -308,7 +308,7 @@ func (unreachable) F() {} // not reachable in export data if err != nil { t.Fatal(err) } - conf := types.Config{Importer: importer.For("source", nil)} + conf := types.Config{Importer: importer.ForCompiler(token.NewFileSet(), "source", nil)} info := &types.Info{ Defs: make(map[*ast.Ident]types.Object), } diff --git a/internal/astutil/clone.go b/internal/astutil/clone.go index d5ee82c58b2..2c9b6bb4841 100644 --- a/internal/astutil/clone.go +++ b/internal/astutil/clone.go @@ -25,7 +25,7 @@ func cloneNode(n ast.Node) ast.Node { } clone = func(x reflect.Value) reflect.Value { switch x.Kind() { - case reflect.Ptr: + case reflect.Pointer: if x.IsNil() { return x } diff --git a/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go index 522287d18d6..37b4a39e9e1 100644 --- a/internal/gcimporter/ureader_yes.go +++ b/internal/gcimporter/ureader_yes.go @@ -574,7 +574,7 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { recv := types.NewVar(fn.Pos(), fn.Pkg(), "", named) typesinternal.SetVarKind(recv, typesinternal.RecvVar) - methods[i] = types.NewFunc(fn.Pos(), fn.Pkg(), fn.Name(), types.NewSignature(recv, sig.Params(), sig.Results(), sig.Variadic())) + methods[i] = types.NewFunc(fn.Pos(), fn.Pkg(), fn.Name(), types.NewSignatureType(recv, nil, nil, sig.Params(), sig.Results(), sig.Variadic())) } embeds := make([]types.Type, iface.NumEmbeddeds()) diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 96fbb8f8706..54308243e1c 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -2981,7 +2981,7 @@ func replaceNode(root ast.Node, from, to ast.Node) { var visit func(reflect.Value) visit = func(v reflect.Value) { switch v.Kind() { - case reflect.Ptr: + case reflect.Pointer: if v.Interface() == from { found = true diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go index 03fb5ccdb17..3be37d5ecde 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -1977,7 +1977,7 @@ func deepHash(n ast.Node) any { var visit func(reflect.Value) visit = func(v reflect.Value) { switch v.Kind() { - case reflect.Ptr: + case reflect.Pointer: ptr := v.UnsafePointer() writeUint64(uint64(uintptr(ptr))) if !v.IsNil() { diff --git a/internal/tool/tool.go b/internal/tool/tool.go index 46f5b87fa35..fe2b1c289b8 100644 --- a/internal/tool/tool.go +++ b/internal/tool/tool.go @@ -250,7 +250,7 @@ func addFlags(f *flag.FlagSet, field reflect.StructField, value reflect.Value) * child := value.Type().Field(i) v := value.Field(i) // make sure we have a pointer - if v.Kind() != reflect.Ptr { + if v.Kind() != reflect.Pointer { v = v.Addr() } // check if that field is a flag or contains flags @@ -289,7 +289,7 @@ func addFlag(f *flag.FlagSet, value reflect.Value, flagName string, help string) func resolve(v reflect.Value) reflect.Value { for { switch v.Kind() { - case reflect.Interface, reflect.Ptr: + case reflect.Interface, reflect.Pointer: v = v.Elem() default: return v diff --git a/refactor/eg/match.go b/refactor/eg/match.go index 31f8af28f23..0a109210bc4 100644 --- a/refactor/eg/match.go +++ b/refactor/eg/match.go @@ -13,8 +13,6 @@ import ( "log" "os" "reflect" - - "golang.org/x/tools/go/ast/astutil" ) // matchExpr reports whether pattern x matches y. @@ -229,7 +227,7 @@ func (tr *Transformer) matchWildcard(xobj *types.Var, y ast.Expr) bool { // -- utilities -------------------------------------------------------- -func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) } +func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } // isRef returns the object referred to by this (possibly qualified) // identifier, or nil if the node is not a referring identifier. diff --git a/refactor/eg/rewrite.go b/refactor/eg/rewrite.go index 3f71c53b7bb..6fb1e44ef30 100644 --- a/refactor/eg/rewrite.go +++ b/refactor/eg/rewrite.go @@ -338,7 +338,7 @@ func (tr *Transformer) subst(env map[string]ast.Expr, pattern, pos reflect.Value } return v - case reflect.Ptr: + case reflect.Pointer: v := reflect.New(p.Type()).Elem() if elem := p.Elem(); elem.IsValid() { v.Set(tr.subst(env, elem, pos).Addr()) diff --git a/refactor/rename/util.go b/refactor/rename/util.go index 7c1a634e4ed..a3d998f90e0 100644 --- a/refactor/rename/util.go +++ b/refactor/rename/util.go @@ -14,8 +14,6 @@ import ( "runtime" "strings" "unicode" - - "golang.org/x/tools/go/ast/astutil" ) func objectKind(obj types.Object) string { @@ -93,7 +91,7 @@ func sameFile(x, y string) bool { return false } -func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) } +func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } func is[T any](x any) bool { _, ok := x.(T) From 44b61a1d174cc06329b20f5de60c2b0c800741a4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 12 Feb 2025 17:33:27 -0500 Subject: [PATCH 17/99] x/tools: eliminate various unparen (et al) helpers This was achieved by annotiating them with //go:fix inline, running gopls/internal/analysis/gofix/main.go -fix ./... then deleting them. Update golang/go#32816 Change-Id: If65dbf8bfcad796ef274d80804daa135e8ccabf9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/648976 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Commit-Queue: Alan Donovan --- go/ssa/builder.go | 20 +++++++-------- go/ssa/emit.go | 2 +- go/ssa/source.go | 2 +- go/ssa/util.go | 2 -- gopls/internal/golang/extract.go | 2 +- gopls/internal/golang/freesymbols.go | 2 +- refactor/eg/match.go | 6 ++--- refactor/rename/spec.go | 4 +-- refactor/rename/util.go | 3 --- refactor/satisfy/find.go | 38 ++++++++++++---------------- 10 files changed, 34 insertions(+), 47 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 4cd71260b61..1761dcc3068 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -559,7 +559,7 @@ func (sb *storebuf) emit(fn *Function) { // literal that may reference parts of the LHS. func (b *builder) assign(fn *Function, loc lvalue, e ast.Expr, isZero bool, sb *storebuf) { // Can we initialize it in place? - if e, ok := unparen(e).(*ast.CompositeLit); ok { + if e, ok := ast.Unparen(e).(*ast.CompositeLit); ok { // A CompositeLit never evaluates to a pointer, // so if the type of the location is a pointer, // an &-operation is implied. @@ -614,7 +614,7 @@ func (b *builder) assign(fn *Function, loc lvalue, e ast.Expr, isZero bool, sb * // expr lowers a single-result expression e to SSA form, emitting code // to fn and returning the Value defined by the expression. func (b *builder) expr(fn *Function, e ast.Expr) Value { - e = unparen(e) + e = ast.Unparen(e) tv := fn.info.Types[e] @@ -704,7 +704,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { return y } // Call to "intrinsic" built-ins, e.g. new, make, panic. - if id, ok := unparen(e.Fun).(*ast.Ident); ok { + if id, ok := ast.Unparen(e.Fun).(*ast.Ident); ok { if obj, ok := fn.info.Uses[id].(*types.Builtin); ok { if v := b.builtin(fn, obj, e.Args, fn.typ(tv.Type), e.Lparen); v != nil { return v @@ -721,7 +721,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { switch e.Op { case token.AND: // &X --- potentially escaping. addr := b.addr(fn, e.X, true) - if _, ok := unparen(e.X).(*ast.StarExpr); ok { + if _, ok := ast.Unparen(e.X).(*ast.StarExpr); ok { // &*p must panic if p is nil (http://golang.org/s/go12nil). // For simplicity, we'll just (suboptimally) rely // on the side effects of a load. @@ -1002,7 +1002,7 @@ func (b *builder) setCallFunc(fn *Function, e *ast.CallExpr, c *CallCommon) { c.pos = e.Lparen // Is this a method call? - if selector, ok := unparen(e.Fun).(*ast.SelectorExpr); ok { + if selector, ok := ast.Unparen(e.Fun).(*ast.SelectorExpr); ok { sel := fn.selection(selector) if sel != nil && sel.kind == types.MethodVal { obj := sel.obj.(*types.Func) @@ -1372,7 +1372,7 @@ func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit, isZero // An &-operation may be implied: // map[*struct{}]bool{&struct{}{}: true} wantAddr := false - if _, ok := unparen(e.Key).(*ast.CompositeLit); ok { + if _, ok := ast.Unparen(e.Key).(*ast.CompositeLit); ok { wantAddr = isPointerCore(t.Key()) } @@ -1547,9 +1547,9 @@ func (b *builder) typeSwitchStmt(fn *Function, s *ast.TypeSwitchStmt, label *lbl var x Value switch ass := s.Assign.(type) { case *ast.ExprStmt: // x.(type) - x = b.expr(fn, unparen(ass.X).(*ast.TypeAssertExpr).X) + x = b.expr(fn, ast.Unparen(ass.X).(*ast.TypeAssertExpr).X) case *ast.AssignStmt: // y := x.(type) - x = b.expr(fn, unparen(ass.Rhs[0]).(*ast.TypeAssertExpr).X) + x = b.expr(fn, ast.Unparen(ass.Rhs[0]).(*ast.TypeAssertExpr).X) } done := fn.newBasicBlock("typeswitch.done") @@ -1667,7 +1667,7 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) { } case *ast.AssignStmt: // x := <-ch - recv := unparen(comm.Rhs[0]).(*ast.UnaryExpr) + recv := ast.Unparen(comm.Rhs[0]).(*ast.UnaryExpr) st = &SelectState{ Dir: types.RecvOnly, Chan: b.expr(fn, recv.X), @@ -1678,7 +1678,7 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) { } case *ast.ExprStmt: // <-ch - recv := unparen(comm.X).(*ast.UnaryExpr) + recv := ast.Unparen(comm.X).(*ast.UnaryExpr) st = &SelectState{ Dir: types.RecvOnly, Chan: b.expr(fn, recv.X), diff --git a/go/ssa/emit.go b/go/ssa/emit.go index a3d41ad95a4..bca79adc4e1 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -81,7 +81,7 @@ func emitDebugRef(f *Function, e ast.Expr, v Value, isAddr bool) { panic("nil") } var obj types.Object - e = unparen(e) + e = ast.Unparen(e) if id, ok := e.(*ast.Ident); ok { if isBlankIdent(id) { return diff --git a/go/ssa/source.go b/go/ssa/source.go index 055a6b1ef5f..d0cc1f4861a 100644 --- a/go/ssa/source.go +++ b/go/ssa/source.go @@ -153,7 +153,7 @@ func findNamedFunc(pkg *Package, pos token.Pos) *Function { // the ssa.Value.) func (f *Function) ValueForExpr(e ast.Expr) (value Value, isAddr bool) { if f.debugInfo() { // (opt) - e = unparen(e) + e = ast.Unparen(e) for _, b := range f.Blocks { for _, instr := range b.Instrs { if ref, ok := instr.(*DebugRef); ok { diff --git a/go/ssa/util.go b/go/ssa/util.go index 56638129602..2a9c9b9d318 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -35,8 +35,6 @@ func assert(p bool, msg string) { //// AST utilities -func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } - // isBlankIdent returns true iff e is an Ident with name "_". // They have no associated types.Object, and thus no type. func isBlankIdent(e ast.Expr) bool { diff --git a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go index 2ce89795a06..8c8758d9f0a 100644 --- a/gopls/internal/golang/extract.go +++ b/gopls/internal/golang/extract.go @@ -1229,7 +1229,7 @@ func collectFreeVars(info *types.Info, file *ast.File, start, end token.Pos, nod // return value acts as an indicator for where it was defined. var sel func(n *ast.SelectorExpr) (types.Object, bool) sel = func(n *ast.SelectorExpr) (types.Object, bool) { - switch x := astutil.Unparen(n.X).(type) { + switch x := ast.Unparen(n.X).(type) { case *ast.SelectorExpr: return sel(x) case *ast.Ident: diff --git a/gopls/internal/golang/freesymbols.go b/gopls/internal/golang/freesymbols.go index 2c9e25165f6..336025367f5 100644 --- a/gopls/internal/golang/freesymbols.go +++ b/gopls/internal/golang/freesymbols.go @@ -342,7 +342,7 @@ func freeRefs(pkg *types.Package, info *types.Info, file *ast.File, start, end t for { suffix = append(suffix, info.Uses[sel.Sel]) - switch x := astutil.Unparen(sel.X).(type) { + switch x := ast.Unparen(sel.X).(type) { case *ast.Ident: return id(x, suffix) default: diff --git a/refactor/eg/match.go b/refactor/eg/match.go index 0a109210bc4..d85a473b978 100644 --- a/refactor/eg/match.go +++ b/refactor/eg/match.go @@ -32,8 +32,8 @@ func (tr *Transformer) matchExpr(x, y ast.Expr) bool { if x == nil || y == nil { return false } - x = unparen(x) - y = unparen(y) + x = ast.Unparen(x) + y = ast.Unparen(y) // Is x a wildcard? (a reference to a 'before' parameter) if xobj, ok := tr.wildcardObj(x); ok { @@ -227,8 +227,6 @@ func (tr *Transformer) matchWildcard(xobj *types.Var, y ast.Expr) bool { // -- utilities -------------------------------------------------------- -func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } - // isRef returns the object referred to by this (possibly qualified) // identifier, or nil if the node is not a referring identifier. func isRef(n ast.Node, info *types.Info) types.Object { diff --git a/refactor/rename/spec.go b/refactor/rename/spec.go index 1d8c32c9dc3..99068c13358 100644 --- a/refactor/rename/spec.go +++ b/refactor/rename/spec.go @@ -155,7 +155,7 @@ func parseObjectSpec(spec *spec, main string) error { } if e, ok := e.(*ast.SelectorExpr); ok { - x := unparen(e.X) + x := ast.Unparen(e.X) // Strip off star constructor, if any. if star, ok := x.(*ast.StarExpr); ok { @@ -172,7 +172,7 @@ func parseObjectSpec(spec *spec, main string) error { if x, ok := x.(*ast.SelectorExpr); ok { // field/method of type e.g. ("encoding/json".Decoder).Decode - y := unparen(x.X) + y := ast.Unparen(x.X) if pkg := parseImportPath(y); pkg != "" { spec.pkg = pkg // e.g. "encoding/json" spec.pkgMember = x.Sel.Name // e.g. "Decoder" diff --git a/refactor/rename/util.go b/refactor/rename/util.go index a3d998f90e0..cb7cea3a86e 100644 --- a/refactor/rename/util.go +++ b/refactor/rename/util.go @@ -5,7 +5,6 @@ package rename import ( - "go/ast" "go/token" "go/types" "os" @@ -91,8 +90,6 @@ func sameFile(x, y string) bool { return false } -func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } - func is[T any](x any) bool { _, ok := x.(T) return ok diff --git a/refactor/satisfy/find.go b/refactor/satisfy/find.go index 3d693aa04ab..a897c3c2fd4 100644 --- a/refactor/satisfy/find.go +++ b/refactor/satisfy/find.go @@ -126,13 +126,13 @@ func (f *Finder) exprN(e ast.Expr) types.Type { case *ast.CallExpr: // x, err := f(args) - sig := coreType(f.expr(e.Fun)).(*types.Signature) + sig := typeparams.CoreType(f.expr(e.Fun)).(*types.Signature) f.call(sig, e.Args) case *ast.IndexExpr: // y, ok := x[i] x := f.expr(e.X) - f.assign(f.expr(e.Index), coreType(x).(*types.Map).Key()) + f.assign(f.expr(e.Index), typeparams.CoreType(x).(*types.Map).Key()) case *ast.TypeAssertExpr: // y, ok := x.(T) @@ -213,7 +213,7 @@ func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Ex f.expr(args[1]) } else { // append(x, y, z) - tElem := coreType(s).(*types.Slice).Elem() + tElem := typeparams.CoreType(s).(*types.Slice).Elem() for _, arg := range args[1:] { f.assign(tElem, f.expr(arg)) } @@ -222,7 +222,7 @@ func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Ex case "delete": m := f.expr(args[0]) k := f.expr(args[1]) - f.assign(coreType(m).(*types.Map).Key(), k) + f.assign(typeparams.CoreType(m).(*types.Map).Key(), k) default: // ordinary call @@ -273,7 +273,7 @@ func (f *Finder) assign(lhs, rhs types.Type) { if types.Identical(lhs, rhs) { return } - if !isInterface(lhs) { + if !types.IsInterface(lhs) { return } @@ -354,7 +354,7 @@ func (f *Finder) expr(e ast.Expr) types.Type { f.sig = saved case *ast.CompositeLit: - switch T := coreType(typeparams.Deref(tv.Type)).(type) { + switch T := typeparams.CoreType(typeparams.Deref(tv.Type)).(type) { case *types.Struct: for i, elem := range e.Elts { if kv, ok := elem.(*ast.KeyValueExpr); ok { @@ -405,7 +405,7 @@ func (f *Finder) expr(e ast.Expr) types.Type { // x[i] or m[k] -- index or lookup operation x := f.expr(e.X) i := f.expr(e.Index) - if ux, ok := coreType(x).(*types.Map); ok { + if ux, ok := typeparams.CoreType(x).(*types.Map); ok { f.assign(ux.Key(), i) } } @@ -440,7 +440,7 @@ func (f *Finder) expr(e ast.Expr) types.Type { // unsafe call. Treat calls to functions in unsafe like ordinary calls, // except that their signature cannot be determined by their func obj. // Without this special handling, f.expr(e.Fun) would fail below. - if s, ok := unparen(e.Fun).(*ast.SelectorExpr); ok { + if s, ok := ast.Unparen(e.Fun).(*ast.SelectorExpr); ok { if obj, ok := f.info.Uses[s.Sel].(*types.Builtin); ok && obj.Pkg().Path() == "unsafe" { sig := f.info.Types[e.Fun].Type.(*types.Signature) f.call(sig, e.Args) @@ -449,7 +449,7 @@ func (f *Finder) expr(e ast.Expr) types.Type { } // builtin call - if id, ok := unparen(e.Fun).(*ast.Ident); ok { + if id, ok := ast.Unparen(e.Fun).(*ast.Ident); ok { if obj, ok := f.info.Uses[id].(*types.Builtin); ok { sig := f.info.Types[id].Type.(*types.Signature) f.builtin(obj, sig, e.Args) @@ -458,7 +458,7 @@ func (f *Finder) expr(e ast.Expr) types.Type { } // ordinary call - f.call(coreType(f.expr(e.Fun)).(*types.Signature), e.Args) + f.call(typeparams.CoreType(f.expr(e.Fun)).(*types.Signature), e.Args) } case *ast.StarExpr: @@ -518,7 +518,7 @@ func (f *Finder) stmt(s ast.Stmt) { case *ast.SendStmt: ch := f.expr(s.Chan) val := f.expr(s.Value) - f.assign(coreType(ch).(*types.Chan).Elem(), val) + f.assign(typeparams.CoreType(ch).(*types.Chan).Elem(), val) case *ast.IncDecStmt: f.expr(s.X) @@ -622,9 +622,9 @@ func (f *Finder) stmt(s ast.Stmt) { var I types.Type switch ass := s.Assign.(type) { case *ast.ExprStmt: // x.(type) - I = f.expr(unparen(ass.X).(*ast.TypeAssertExpr).X) + I = f.expr(ast.Unparen(ass.X).(*ast.TypeAssertExpr).X) case *ast.AssignStmt: // y := x.(type) - I = f.expr(unparen(ass.Rhs[0]).(*ast.TypeAssertExpr).X) + I = f.expr(ast.Unparen(ass.Rhs[0]).(*ast.TypeAssertExpr).X) } for _, cc := range s.Body.List { cc := cc.(*ast.CaseClause) @@ -668,7 +668,7 @@ func (f *Finder) stmt(s ast.Stmt) { var xelem types.Type // Keys of array, *array, slice, string aren't interesting // since the RHS key type is just an int. - switch ux := coreType(x).(type) { + switch ux := typeparams.CoreType(x).(type) { case *types.Chan: xelem = ux.Elem() case *types.Map: @@ -683,13 +683,13 @@ func (f *Finder) stmt(s ast.Stmt) { var xelem types.Type // Values of type strings aren't interesting because // the RHS value type is just a rune. - switch ux := coreType(x).(type) { + switch ux := typeparams.CoreType(x).(type) { case *types.Array: xelem = ux.Elem() case *types.Map: xelem = ux.Elem() case *types.Pointer: // *array - xelem = coreType(typeparams.Deref(ux)).(*types.Array).Elem() + xelem = typeparams.CoreType(typeparams.Deref(ux)).(*types.Array).Elem() case *types.Slice: xelem = ux.Elem() } @@ -707,12 +707,6 @@ func (f *Finder) stmt(s ast.Stmt) { // -- Plundered from golang.org/x/tools/go/ssa ----------------- -func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } - -func isInterface(T types.Type) bool { return types.IsInterface(T) } - -func coreType(T types.Type) types.Type { return typeparams.CoreType(T) } - func instance(info *types.Info, expr ast.Expr) bool { var id *ast.Ident switch x := expr.(type) { From ddd4bde5a0f514414daf47ff0232b601ba01d18f Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 13 Feb 2025 18:02:22 +0000 Subject: [PATCH 18/99] gopls/internal/golang: avoid PackageSymbols errors with missing packages As reported in golang/vscode-go#3681, spurious errors from gopls.package_symbols can cause very distracting popups in VS Code. For now, err on the side of silence. In the future, we may want to revisit this behavior. For golang/vscode-go#3681 Change-Id: I67f8b8e1e299ef88dabbb284a151aada131652f8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649257 Reviewed-by: Hongxiang Jiang LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/symbols.go | 32 +++--- gopls/internal/server/command.go | 4 + .../integration/misc/package_symbols_test.go | 101 ++++++++++-------- 3 files changed, 80 insertions(+), 57 deletions(-) diff --git a/gopls/internal/golang/symbols.go b/gopls/internal/golang/symbols.go index 14f2703441c..db31baa69f2 100644 --- a/gopls/internal/golang/symbols.go +++ b/gopls/internal/golang/symbols.go @@ -86,17 +86,22 @@ func PackageSymbols(ctx context.Context, snapshot *cache.Snapshot, uri protocol. ctx, done := event.Start(ctx, "source.PackageSymbols") defer done() - mp, err := NarrowestMetadataForFile(ctx, snapshot, uri) - if err != nil { - return command.PackageSymbolsResult{}, err + pkgFiles := []protocol.DocumentURI{uri} + + // golang/vscode-go#3681: do our best if the file is not in a package. + // TODO(rfindley): revisit this in the future once there is more graceful + // handling in VS Code. + if mp, err := NarrowestMetadataForFile(ctx, snapshot, uri); err == nil { + pkgFiles = mp.CompiledGoFiles } - pkgfiles := mp.CompiledGoFiles - // Maps receiver name to the methods that use it - receiverToMethods := make(map[string][]command.PackageSymbol) - // Maps type symbol name to its index in symbols - typeSymbolToIdx := make(map[string]int) - var symbols []command.PackageSymbol - for fidx, f := range pkgfiles { + + var ( + pkgName string + symbols []command.PackageSymbol + receiverToMethods = make(map[string][]command.PackageSymbol) // receiver name -> methods + typeSymbolToIdx = make(map[string]int) // type name -> index in symbols + ) + for fidx, f := range pkgFiles { fh, err := snapshot.ReadFile(ctx, f) if err != nil { return command.PackageSymbolsResult{}, err @@ -105,6 +110,9 @@ func PackageSymbols(ctx context.Context, snapshot *cache.Snapshot, uri protocol. if err != nil { return command.PackageSymbolsResult{}, err } + if pkgName == "" && pgf.File != nil && pgf.File.Name != nil { + pkgName = pgf.File.Name.Name + } for _, decl := range pgf.File.Decls { switch decl := decl.(type) { case *ast.FuncDecl: @@ -154,8 +162,8 @@ func PackageSymbols(ctx context.Context, snapshot *cache.Snapshot, uri protocol. } } return command.PackageSymbolsResult{ - PackageName: string(mp.Name), - Files: pkgfiles, + PackageName: pkgName, + Files: pkgFiles, Symbols: symbols, }, nil diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 2b5c282a28f..007b8d5218f 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -1741,6 +1741,10 @@ func (c *commandHandler) PackageSymbols(ctx context.Context, args command.Packag err := c.run(ctx, commandConfig{ forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { + if deps.snapshot.FileKind(deps.fh) != file.Go { + // golang/vscode-go#3681: fail silently, to avoid spurious error popups. + return nil + } res, err := golang.PackageSymbols(ctx, deps.snapshot, args.URI) if err != nil { return err diff --git a/gopls/internal/test/integration/misc/package_symbols_test.go b/gopls/internal/test/integration/misc/package_symbols_test.go index 860264f2bb0..1e06a655935 100644 --- a/gopls/internal/test/integration/misc/package_symbols_test.go +++ b/gopls/internal/test/integration/misc/package_symbols_test.go @@ -16,6 +16,11 @@ import ( func TestPackageSymbols(t *testing.T) { const files = ` +-- go.mod -- +module example.com + +go 1.20 + -- a.go -- package a @@ -33,68 +38,74 @@ func (s *S) M2() {} func (s *S) M3() {} func F() {} +-- unloaded.go -- +//go:build unloaded + +package a + +var Unloaded int ` integration.Run(t, files, func(t *testing.T, env *integration.Env) { - a_uri := env.Sandbox.Workdir.URI("a.go") - b_uri := env.Sandbox.Workdir.URI("b.go") + aURI := env.Sandbox.Workdir.URI("a.go") + bURI := env.Sandbox.Workdir.URI("b.go") args, err := command.MarshalArgs(command.PackageSymbolsArgs{ - URI: a_uri, + URI: aURI, }) if err != nil { - t.Fatalf("failed to MarshalArgs: %v", err) + t.Fatal(err) } var res command.PackageSymbolsResult env.ExecuteCommand(&protocol.ExecuteCommandParams{ - Command: "gopls.package_symbols", + Command: command.PackageSymbols.String(), Arguments: args, }, &res) want := command.PackageSymbolsResult{ PackageName: "a", - Files: []protocol.DocumentURI{a_uri, b_uri}, + Files: []protocol.DocumentURI{aURI, bURI}, Symbols: []command.PackageSymbol{ - { - Name: "A", - Kind: protocol.Variable, - File: 0, - }, - { - Name: "F", - Kind: protocol.Function, - File: 1, - }, - { - Name: "S", - Kind: protocol.Struct, - File: 0, - Children: []command.PackageSymbol{ - { - Name: "M1", - Kind: protocol.Method, - File: 0, - }, - { - Name: "M2", - Kind: protocol.Method, - File: 1, - }, - { - Name: "M3", - Kind: protocol.Method, - File: 1, - }, - }, - }, - { - Name: "b", - Kind: protocol.Variable, - File: 1, - }, + {Name: "A", Kind: protocol.Variable, File: 0}, + {Name: "F", Kind: protocol.Function, File: 1}, + {Name: "S", Kind: protocol.Struct, File: 0, Children: []command.PackageSymbol{ + {Name: "M1", Kind: protocol.Method, File: 0}, + {Name: "M2", Kind: protocol.Method, File: 1}, + {Name: "M3", Kind: protocol.Method, File: 1}, + }}, + {Name: "b", Kind: protocol.Variable, File: 1}, }, } - if diff := cmp.Diff(want, res, cmpopts.IgnoreFields(command.PackageSymbol{}, "Range", "SelectionRange", "Detail")); diff != "" { - t.Errorf("gopls.package_symbols returned unexpected diff (-want +got):\n%s", diff) + ignore := cmpopts.IgnoreFields(command.PackageSymbol{}, "Range", "SelectionRange", "Detail") + if diff := cmp.Diff(want, res, ignore); diff != "" { + t.Errorf("package_symbols returned unexpected diff (-want +got):\n%s", diff) + } + + for file, want := range map[string]command.PackageSymbolsResult{ + "go.mod": {}, + "unloaded.go": { + PackageName: "a", + Files: []protocol.DocumentURI{env.Sandbox.Workdir.URI("unloaded.go")}, + Symbols: []command.PackageSymbol{ + {Name: "Unloaded", Kind: protocol.Variable, File: 0}, + }, + }, + } { + uri := env.Sandbox.Workdir.URI(file) + args, err := command.MarshalArgs(command.PackageSymbolsArgs{ + URI: uri, + }) + if err != nil { + t.Fatal(err) + } + var res command.PackageSymbolsResult + env.ExecuteCommand(&protocol.ExecuteCommandParams{ + Command: command.PackageSymbols.String(), + Arguments: args, + }, &res) + + if diff := cmp.Diff(want, res, ignore); diff != "" { + t.Errorf("package_symbols returned unexpected diff (-want +got):\n%s", diff) + } } }) } From ab04c1963f5c2e5425c494f549e018313bdaa817 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 13 Feb 2025 14:37:56 -0500 Subject: [PATCH 19/99] gopls/internal/analysis/modernize: improve rangeint transformation for i := 0; i < len(slice); i++ {} is currently reduced to for i := range len(slice) {} but this CL delivers the better style of: for i := range slice {} Fixes golang/go#71725 Change-Id: Idb025315047c3be992267f6c1783757798e0c840 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649356 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- gopls/internal/analysis/modernize/rangeint.go | 10 ++++++++++ .../modernize/testdata/src/rangeint/rangeint.go | 5 ++++- .../modernize/testdata/src/rangeint/rangeint.go.golden | 5 ++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/gopls/internal/analysis/modernize/rangeint.go b/gopls/internal/analysis/modernize/rangeint.go index c36203cef06..2d25d6a0a06 100644 --- a/gopls/internal/analysis/modernize/rangeint.go +++ b/gopls/internal/analysis/modernize/rangeint.go @@ -8,10 +8,12 @@ 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/inspector" + "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/astutil/edge" @@ -98,6 +100,14 @@ func rangeint(pass *analysis.Pass) { }) } + // If limit is len(slice), + // simplify "range len(slice)" to "range slice". + if call, ok := limit.(*ast.CallExpr); ok && + typeutil.Callee(info, call) == builtinLen && + is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) { + limit = call.Args[0] + } + pass.Report(analysis.Diagnostic{ Pos: init.Pos(), End: inc.End(), diff --git a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go index e17dccac9d0..a60bd5eac37 100644 --- a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go +++ b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go @@ -1,6 +1,6 @@ package rangeint -func _(i int, s struct{ i int }) { +func _(i int, s struct{ i int }, slice []int) { for i := 0; i < 10; i++ { // want "for loop can be modernized using range over int" println(i) } @@ -9,6 +9,9 @@ func _(i int, s struct{ i int }) { for i := 0; i < 10; i++ { // want "for loop can be modernized using range over int" // i unused within loop } + for i := 0; i < len(slice); i++ { // want "for loop can be modernized using range over int" + println(slice[i]) + } // nope for i := 0; i < 10; { // nope: missing increment diff --git a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden index 5a76229c858..348f77508ac 100644 --- a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden @@ -1,6 +1,6 @@ package rangeint -func _(i int, s struct{ i int }) { +func _(i int, s struct{ i int }, slice []int) { for i := range 10 { // want "for loop can be modernized using range over int" println(i) } @@ -9,6 +9,9 @@ func _(i int, s struct{ i int }) { for range 10 { // want "for loop can be modernized using range over int" // i unused within loop } + for i := range slice { // want "for loop can be modernized using range over int" + println(slice[i]) + } // nope for i := 0; i < 10; { // nope: missing increment From 809cde44e486bf9b083f7c68d47e09fc20662b4f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 13 Feb 2025 13:37:07 -0500 Subject: [PATCH 20/99] gopls/internal/analysis/modernize: fix bug in minmax Wrong operator. D'oh. + test Fixes golang/go#71721 Change-Id: Ia7fe314df07afa9a9de63c2b6031e678755e9d56 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649357 Reviewed-by: Jonathan Amsterdam Reviewed-by: Alan Donovan Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/modernize/minmax.go | 2 +- .../analysis/modernize/testdata/src/minmax/minmax.go | 11 +++++++++++ .../modernize/testdata/src/minmax/minmax.go.golden | 11 +++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/gopls/internal/analysis/modernize/minmax.go b/gopls/internal/analysis/modernize/minmax.go index 26b12341cad..1466e767fc7 100644 --- a/gopls/internal/analysis/modernize/minmax.go +++ b/gopls/internal/analysis/modernize/minmax.go @@ -57,7 +57,7 @@ func minmax(pass *analysis.Pass) { if equalSyntax(lhs, lhs2) { if equalSyntax(rhs, a) && equalSyntax(rhs2, b) { sign = +sign - } else if equalSyntax(rhs2, a) || equalSyntax(rhs, b) { + } else if equalSyntax(rhs2, a) && equalSyntax(rhs, b) { sign = -sign } else { return diff --git a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go index c73bd30139b..8fdc3bc2106 100644 --- a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go +++ b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go @@ -92,3 +92,14 @@ func nopeAssignHasIncrementOperator() { } print(y) } + +// Regression test for https://github.com/golang/go/issues/71721. +func nopeNotAMinimum(x, y int) int { + // A value of -1 or 0 will use a default value (30). + if x <= 0 { + y = 30 + } else { + y = x + } + return y +} diff --git a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden index 11eac2c1418..48e154729e7 100644 --- a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden @@ -69,3 +69,14 @@ func nopeAssignHasIncrementOperator() { } print(y) } + +// Regression test for https://github.com/golang/go/issues/71721. +func nopeNotAMinimum(x, y int) int { + // A value of -1 or 0 will use a default value (30). + if x <= 0 { + y = 30 + } else { + y = x + } + return y +} From 85a3833c521302254b403f7606939863e21f736a Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Wed, 12 Feb 2025 11:18:11 -0500 Subject: [PATCH 21/99] internal/analysis/gofix: simple type aliases Offer to replace inlinable type aliases whose RHS is a named type. Despite the amount of new code, there is little to test, because most of it duplicates constants. When there is a chain of inlinable type aliases, as in: type A = T type AA = A var v AA we don't follow the chain to the end. Instead, the first set of fixes produces: type A = T type AA = T var v A That is, the replacements happen "in parallel." A second round of fixes would rewrite var v A to var v T This case is rare enough that it's not worth doing better. For golang/go#32816. Change-Id: Ib6854d60b26a273b592a297cb9a650a31e094392 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649055 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/gofix/gofix.go | 264 +++++++++++++----- .../analysis/gofix/testdata/src/a/a.go | 15 + .../analysis/gofix/testdata/src/a/a.go.golden | 15 + .../analysis/gofix/testdata/src/b/b.go | 2 + .../analysis/gofix/testdata/src/b/b.go.golden | 2 + 5 files changed, 225 insertions(+), 73 deletions(-) diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index 8ec31bd4736..147399d315d 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -63,8 +63,11 @@ func run(pass *analysis.Pass) (any, error) { // Pass 1: find functions and constants annotated with an appropriate "//go:fix" // comment (the syntax proposed by #32816), // and export a fact for each one. - inlinableFuncs := make(map[*types.Func]*inline.Callee) // memoization of fact import (nil => no fact) - inlinableConsts := make(map[*types.Const]*goFixInlineConstFact) + var ( + inlinableFuncs = make(map[*types.Func]*inline.Callee) // memoization of fact import (nil => no fact) + inlinableConsts = make(map[*types.Const]*goFixInlineConstFact) + inlinableAliases = make(map[*types.TypeName]*goFixInlineAliasFact) + ) inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{(*ast.FuncDecl)(nil), (*ast.GenDecl)(nil)} @@ -89,52 +92,96 @@ func run(pass *analysis.Pass) (any, error) { inlinableFuncs[fn] = callee case *ast.GenDecl: - if decl.Tok != token.CONST { + if decl.Tok != token.CONST && decl.Tok != token.TYPE { return } declInline := hasFixInline(decl.Doc) // Accept inline directives on the entire decl as well as individual specs. for _, spec := range decl.Specs { - spec := spec.(*ast.ValueSpec) // guaranteed by Tok == CONST - specInline := hasFixInline(spec.Doc) - if declInline || specInline { - for i, name := range spec.Names { - if i >= len(spec.Values) { - // Possible following an iota. - break - } - val := spec.Values[i] - var rhsID *ast.Ident - switch e := val.(type) { - case *ast.Ident: - // Constants defined with the predeclared iota cannot be inlined. - if pass.TypesInfo.Uses[e] == builtinIota { - pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is iota") + switch spec := spec.(type) { + case *ast.TypeSpec: // Tok == TYPE + if !declInline && !hasFixInline(spec.Doc) { + continue + } + if !spec.Assign.IsValid() { + pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: not a type alias") + continue + } + if spec.TypeParams != nil { + // TODO(jba): handle generic aliases + continue + } + // The alias must refer to another named type. + // TODO(jba): generalize to more type expressions. + var rhsID *ast.Ident + switch e := ast.Unparen(spec.Type).(type) { + case *ast.Ident: + rhsID = e + case *ast.SelectorExpr: + rhsID = e.Sel + default: + continue + } + lhs := pass.TypesInfo.Defs[spec.Name].(*types.TypeName) + // more (jba): test one alias pointing to another alias + rhs := pass.TypesInfo.Uses[rhsID].(*types.TypeName) + typ := &goFixInlineAliasFact{ + RHSName: rhs.Name(), + RHSPkgName: rhs.Pkg().Name(), + RHSPkgPath: rhs.Pkg().Path(), + } + if rhs.Pkg() == pass.Pkg { + typ.rhsObj = rhs + } + inlinableAliases[lhs] = typ + // Create a fact only if the LHS is exported and defined at top level. + // We create a fact even if the RHS is non-exported, + // so we can warn about uses in other packages. + if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { + pass.ExportObjectFact(lhs, typ) + } + + case *ast.ValueSpec: // Tok == CONST + specInline := hasFixInline(spec.Doc) + if declInline || specInline { + for i, name := range spec.Names { + if i >= len(spec.Values) { + // Possible following an iota. + break + } + val := spec.Values[i] + var rhsID *ast.Ident + switch e := val.(type) { + case *ast.Ident: + // Constants defined with the predeclared iota cannot be inlined. + if pass.TypesInfo.Uses[e] == builtinIota { + pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is iota") + continue + } + rhsID = e + case *ast.SelectorExpr: + rhsID = e.Sel + default: + pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is not the name of another constant") continue } - rhsID = e - case *ast.SelectorExpr: - rhsID = e.Sel - default: - pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is not the name of another constant") - continue - } - lhs := pass.TypesInfo.Defs[name].(*types.Const) - rhs := pass.TypesInfo.Uses[rhsID].(*types.Const) // must be so in a well-typed program - con := &goFixInlineConstFact{ - RHSName: rhs.Name(), - RHSPkgName: rhs.Pkg().Name(), - RHSPkgPath: rhs.Pkg().Path(), - } - if rhs.Pkg() == pass.Pkg { - con.rhsObj = rhs - } - inlinableConsts[lhs] = con - // Create a fact only if the LHS is exported and defined at top level. - // We create a fact even if the RHS is non-exported, - // so we can warn uses in other packages. - if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { - pass.ExportObjectFact(lhs, con) + lhs := pass.TypesInfo.Defs[name].(*types.Const) + rhs := pass.TypesInfo.Uses[rhsID].(*types.Const) // must be so in a well-typed program + con := &goFixInlineConstFact{ + RHSName: rhs.Name(), + RHSPkgName: rhs.Pkg().Name(), + RHSPkgPath: rhs.Pkg().Path(), + } + if rhs.Pkg() == pass.Pkg { + con.rhsObj = rhs + } + inlinableConsts[lhs] = con + // Create a fact only if the LHS is exported and defined at top level. + // We create a fact even if the RHS is non-exported, + // so we can warn about uses in other packages. + if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { + pass.ExportObjectFact(lhs, con) + } } } } @@ -143,7 +190,7 @@ func run(pass *analysis.Pass) (any, error) { }) // Pass 2. Inline each static call to an inlinable function - // and each reference to an inlinable constant. + // and each reference to an inlinable constant or type alias. // // TODO(adonovan): handle multiple diffs that each add the same import. for cur := range cursor.Root(inspect).Preorder((*ast.CallExpr)(nil), (*ast.Ident)(nil)) { @@ -218,6 +265,65 @@ func run(pass *analysis.Pass) (any, error) { } case *ast.Ident: + // If the identifier is a use of an inlinable type alias, suggest inlining it. + // TODO(jba): much of this code is shared with the constant case, below. + // Try to factor more of it out, unless it will change anyway when we move beyond simple RHS's. + if ali, ok := pass.TypesInfo.Uses[n].(*types.TypeName); ok { + inalias, ok := inlinableAliases[ali] + if !ok { + var fact goFixInlineAliasFact + if pass.ImportObjectFact(ali, &fact) { + inalias = &fact + inlinableAliases[ali] = inalias + } + } + if inalias == nil { + continue // nope + } + curFile := currentFile(cur) + + // We have an identifier A here (n), possibly qualified by a package identifier (sel.X, + // where sel is the parent of X), // and an inlinable "type A = B" elsewhere (inali). + // Consider replacing A with B. + + // Check that the expression we are inlining (B) means the same thing + // (refers to the same object) in n's scope as it does in A's scope. + // If the RHS is not in the current package, AddImport will handle + // shadowing, so we only need to worry about when both expressions + // are in the current package. + if pass.Pkg.Path() == inalias.RHSPkgPath { + // fcon.rhsObj is the object referred to by B in the definition of A. + scope := pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope + _, obj := scope.LookupParent(inalias.RHSName, n.Pos()) // what "B" means in n's scope + if obj == nil { + // Should be impossible: if code at n can refer to the LHS, + // it can refer to the RHS. + panic(fmt.Sprintf("no object for inlinable alias %s RHS %s", n.Name, inalias.RHSName)) + } + if obj != inalias.rhsObj { + // "B" means something different here than at the inlinable const's scope. + continue + } + } else if !analysisinternal.CanImport(pass.Pkg.Path(), inalias.RHSPkgPath) { + // If this package can't see the RHS's package, we can't inline. + continue + } + var ( + importPrefix string + edits []analysis.TextEdit + ) + if inalias.RHSPkgPath != pass.Pkg.Path() { + _, importPrefix, edits = analysisinternal.AddImport( + pass.TypesInfo, curFile, inalias.RHSPkgName, inalias.RHSPkgPath, inalias.RHSName, n.Pos()) + } + // If n is qualified by a package identifier, we'll need the full selector expression. + var expr ast.Expr = n + if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { + expr = cur.Parent().Node().(ast.Expr) + } + reportInline(pass, "type alias", "Type alias", expr, edits, importPrefix+inalias.RHSName) + continue + } // If the identifier is a use of an inlinable constant, suggest inlining it. if con, ok := pass.TypesInfo.Uses[n].(*types.Const); ok { incon, ok := inlinableConsts[con] @@ -233,14 +339,10 @@ func run(pass *analysis.Pass) (any, error) { } // If n is qualified by a package identifier, we'll need the full selector expression. - var sel *ast.SelectorExpr - if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { - sel = cur.Parent().Node().(*ast.SelectorExpr) - } curFile := currentFile(cur) - // We have an identifier A here (n), possibly qualified by a package identifier (sel.X), - // and an inlinable "const A = B" elsewhere (fcon). + // We have an identifier A here (n), possibly qualified by a package identifier (sel.X, + // where sel is the parent of n), // and an inlinable "const A = B" elsewhere (incon). // Consider replacing A with B. // Check that the expression we are inlining (B) means the same thing @@ -249,7 +351,7 @@ func run(pass *analysis.Pass) (any, error) { // shadowing, so we only need to worry about when both expressions // are in the current package. if pass.Pkg.Path() == incon.RHSPkgPath { - // fcon.rhsObj is the object referred to by B in the definition of A. + // incon.rhsObj is the object referred to by B in the definition of A. scope := pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope _, obj := scope.LookupParent(incon.RHSName, n.Pos()) // what "B" means in n's scope if obj == nil { @@ -273,31 +375,12 @@ func run(pass *analysis.Pass) (any, error) { _, importPrefix, edits = analysisinternal.AddImport( pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos()) } - var ( - pos = n.Pos() - end = n.End() - name = n.Name - ) - // Replace the entire SelectorExpr if there is one. - if sel != nil { - pos = sel.Pos() - end = sel.End() - name = sel.X.(*ast.Ident).Name + "." + n.Name + // If n is qualified by a package identifier, we'll need the full selector expression. + var expr ast.Expr = n + if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { + expr = cur.Parent().Node().(ast.Expr) } - edits = append(edits, analysis.TextEdit{ - Pos: pos, - End: end, - NewText: []byte(importPrefix + incon.RHSName), - }) - pass.Report(analysis.Diagnostic{ - Pos: pos, - End: end, - Message: fmt.Sprintf("Constant %s should be inlined", name), - SuggestedFixes: []analysis.SuggestedFix{{ - Message: fmt.Sprintf("Inline constant %s", name), - TextEdits: edits, - }}, - }) + reportInline(pass, "constant", "Constant", expr, edits, importPrefix+incon.RHSName) } } } @@ -305,6 +388,25 @@ func run(pass *analysis.Pass) (any, error) { return nil, nil } +// reportInline reports a diagnostic for fixing an inlinable name. +func reportInline(pass *analysis.Pass, kind, capKind string, ident ast.Expr, edits []analysis.TextEdit, newText string) { + edits = append(edits, analysis.TextEdit{ + Pos: ident.Pos(), + End: ident.End(), + NewText: []byte(newText), + }) + name := analysisinternal.Format(pass.Fset, ident) + pass.Report(analysis.Diagnostic{ + Pos: ident.Pos(), + End: ident.End(), + Message: fmt.Sprintf("%s %s should be inlined", capKind, name), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Inline %s %s", kind, name), + TextEdits: edits, + }}, + }) +} + // hasFixInline reports the presence of a "//go:fix inline" directive // in the comments. func hasFixInline(cg *ast.CommentGroup) bool { @@ -339,6 +441,22 @@ func (c *goFixInlineConstFact) String() string { func (*goFixInlineConstFact) AFact() {} +// A goFixInlineAliasFact is exported for each type alias marked "//go:fix inline". +// It holds information about an inlinable type alias. Gob-serializable. +type goFixInlineAliasFact struct { + // Information about "type LHSName = RHSName". + RHSName string + RHSPkgPath string + RHSPkgName string + rhsObj types.Object // for current package +} + +func (c *goFixInlineAliasFact) String() string { + return fmt.Sprintf("goFixInline alias %q.%s", c.RHSPkgPath, c.RHSName) +} + +func (*goFixInlineAliasFact) AFact() {} + func discard(string, ...any) {} var builtinIota = types.Universe.Lookup("iota") diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go b/gopls/internal/analysis/gofix/testdata/src/a/a.go index 4f41b9a8c5d..fb4d8b92172 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/a.go +++ b/gopls/internal/analysis/gofix/testdata/src/a/a.go @@ -101,3 +101,18 @@ func shadow() { _ = x } + +// Type aliases + +//go:fix inline +type A = T // want A: `goFixInline alias "a".T` + +var _ A // want `Type alias A should be inlined` + +type B = []T // nope: only named RHSs + +//go:fix inline +type AA = // want AA: `goFixInline alias "a".A` +A // want `Type alias A should be inlined` + +var _ AA // want `Type alias AA should be inlined` diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden index 9e9cc25996f..9ab1bcbc652 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden +++ b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden @@ -101,3 +101,18 @@ func shadow() { _ = x } + +// Type aliases + +//go:fix inline +type A = T // want A: `goFixInline alias "a".T` + +var _ T // want `Type alias A should be inlined` + +type B = []T // nope: only named RHSs + +//go:fix inline +type AA = // want AA: `goFixInline alias "a".A` +T // want `Type alias A should be inlined` + +var _ A // want `Type alias AA should be inlined` diff --git a/gopls/internal/analysis/gofix/testdata/src/b/b.go b/gopls/internal/analysis/gofix/testdata/src/b/b.go index 74876738bea..d52fd514024 100644 --- a/gopls/internal/analysis/gofix/testdata/src/b/b.go +++ b/gopls/internal/analysis/gofix/testdata/src/b/b.go @@ -30,3 +30,5 @@ func g() { } const d = a.D // nope: a.D refers to a constant in a package that is not visible here. + +var _ a.A // want `Type alias a\.A should be inlined` diff --git a/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden b/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden index b3608d6793e..4228ffeb489 100644 --- a/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden +++ b/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden @@ -34,3 +34,5 @@ func g() { } const d = a.D // nope: a.D refers to a constant in a package that is not visible here. + +var _ a.T // want `Type alias a\.A should be inlined` From c0dbb60e4ba78287d76e623a94ae61616ff3c74c Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 13 Feb 2025 16:29:14 -0500 Subject: [PATCH 22/99] gopls: tweak release notes Also, move gofix command into a package so that it can be "go run" from the release branch. Change-Id: I0a75c1ec4b00d22eef6c13c5162dd02ed9ef272f Reviewed-on: https://go-review.googlesource.com/c/tools/+/649318 Auto-Submit: Alan Donovan Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- gopls/doc/analyzers.md | 38 ++++++++++++++++--- gopls/doc/release/v0.18.0.md | 18 +++++++-- .../analysis/gofix/{ => cmd/gofix}/main.go | 5 +-- gopls/internal/analysis/gofix/doc.go | 4 ++ gopls/internal/analysis/modernize/doc.go | 9 +++++ gopls/internal/analysis/unusedfunc/doc.go | 29 +++++++++++--- gopls/internal/doc/api.json | 8 ++-- 7 files changed, 88 insertions(+), 23 deletions(-) rename gopls/internal/analysis/gofix/{ => cmd/gofix}/main.go (83%) diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 68465f9809d..dde95591718 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -500,6 +500,15 @@ existing code by using more modern features of Go, such as: - replacing Split in "for range strings.Split(...)" by go1.24's more efficient SplitSeq; +To apply all modernization fixes en masse, you can use the +following command: + + $ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./... + +If the tool warns of conflicting fixes, you may need to run it more +than once until it has applied all fixes cleanly. This command is +not an officially supported interface and may change in the future. + Default: on. Package documentation: [modernize](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize) @@ -962,12 +971,29 @@ A method is considered unused if it is unexported, not referenced that of any method of an interface type declared within the same package. -The tool may report a false positive for a declaration of an -unexported function that is referenced from another package using -the go:linkname mechanism, if the declaration's doc comment does -not also have a go:linkname comment. (Such code is in any case -strongly discouraged: linkname annotations, if they must be used at -all, should be used on both the declaration and the alias.) +The tool may report false positives in some situations, for +example: + + - For a declaration of an unexported function that is referenced + from another package using the go:linkname mechanism, if the + declaration's doc comment does not also have a go:linkname + comment. + + (Such code is in any case strongly discouraged: linkname + annotations, if they must be used at all, should be used on both + the declaration and the alias.) + + - For compiler intrinsics in the "runtime" package that, though + never referenced, are known to the compiler and are called + indirectly by compiled object code. + + - For functions called only from assembly. + + - For functions called only from files whose build tags are not + selected in the current build configuration. + +See https://github.com/golang/go/issues/71686 for discussion of +these limitations. The unusedfunc algorithm is not as precise as the golang.org/x/tools/cmd/deadcode tool, but it has the advantage that diff --git a/gopls/doc/release/v0.18.0.md b/gopls/doc/release/v0.18.0.md index 8d641a2104f..ba2c0184307 100644 --- a/gopls/doc/release/v0.18.0.md +++ b/gopls/doc/release/v0.18.0.md @@ -37,16 +37,22 @@ details to be reported as diagnostics. For example, it indicates which variables escape to the heap, and which array accesses require bounds checks. +TODO: add links to the complete manual for each item. + ## New `modernize` analyzer Gopls now reports when code could be simplified or clarified by using more modern features of Go, and provides a quick fix to apply the change. -Examples: +For example, a conditional assignment using an if/else statement may +be replaced by a call to the `min` or `max` built-in functions added +in Go 1.18. -- replacement of conditional assignment using an if/else statement by - a call to the `min` or `max` built-in functions added in Go 1.18; +Use this command to apply modernization fixes en masse: +``` +$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./... +``` ## New `unusedfunc` analyzer @@ -97,6 +103,12 @@ const Ptr = Pointer ``` gopls will suggest replacing `Ptr` in your code with `Pointer`. +Use this command to apply such fixes en masse: + +``` +$ go run golang.org/x/tools/gopls/internal/analysis/gofix/cmd/gofix@latest -test ./... +``` + ## "Implementations" supports generics At long last, the "Go to Implementations" feature now fully supports diff --git a/gopls/internal/analysis/gofix/main.go b/gopls/internal/analysis/gofix/cmd/gofix/main.go similarity index 83% rename from gopls/internal/analysis/gofix/main.go rename to gopls/internal/analysis/gofix/cmd/gofix/main.go index fde633f2f62..d75978f6e59 100644 --- a/gopls/internal/analysis/gofix/main.go +++ b/gopls/internal/analysis/gofix/cmd/gofix/main.go @@ -1,10 +1,7 @@ -// Copyright 2023 The Go Authors. All rights reserved. +// 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. -//go:build ignore -// +build ignore - // The inline command applies the inliner to the specified packages of // Go source code. Run with: // diff --git a/gopls/internal/analysis/gofix/doc.go b/gopls/internal/analysis/gofix/doc.go index a0c6a08ded9..ad8b067daa4 100644 --- a/gopls/internal/analysis/gofix/doc.go +++ b/gopls/internal/analysis/gofix/doc.go @@ -77,5 +77,9 @@ or before a group, applying to every constant in the group: ) The proposal https://go.dev/issue/32816 introduces the "//go:fix" directives. + +You can use this (officially unsupported) command to apply gofix fixes en masse: + + $ go run golang.org/x/tools/gopls/internal/analysis/gofix/cmd/gofix@latest -test ./... */ package gofix diff --git a/gopls/internal/analysis/modernize/doc.go b/gopls/internal/analysis/modernize/doc.go index 15aeab64d8d..3759fdb10c5 100644 --- a/gopls/internal/analysis/modernize/doc.go +++ b/gopls/internal/analysis/modernize/doc.go @@ -32,4 +32,13 @@ // for i := range n {}, added in go1.22; // - replacing Split in "for range strings.Split(...)" by go1.24's // more efficient SplitSeq; +// +// To apply all modernization fixes en masse, you can use the +// following command: +// +// $ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./... +// +// If the tool warns of conflicting fixes, you may need to run it more +// than once until it has applied all fixes cleanly. This command is +// not an officially supported interface and may change in the future. package modernize diff --git a/gopls/internal/analysis/unusedfunc/doc.go b/gopls/internal/analysis/unusedfunc/doc.go index 5946ed897bb..9e2fc8145c8 100644 --- a/gopls/internal/analysis/unusedfunc/doc.go +++ b/gopls/internal/analysis/unusedfunc/doc.go @@ -20,12 +20,29 @@ // that of any method of an interface type declared within the same // package. // -// The tool may report a false positive for a declaration of an -// unexported function that is referenced from another package using -// the go:linkname mechanism, if the declaration's doc comment does -// not also have a go:linkname comment. (Such code is in any case -// strongly discouraged: linkname annotations, if they must be used at -// all, should be used on both the declaration and the alias.) +// The tool may report false positives in some situations, for +// example: +// +// - For a declaration of an unexported function that is referenced +// from another package using the go:linkname mechanism, if the +// declaration's doc comment does not also have a go:linkname +// comment. +// +// (Such code is in any case strongly discouraged: linkname +// annotations, if they must be used at all, should be used on both +// the declaration and the alias.) +// +// - For compiler intrinsics in the "runtime" package that, though +// never referenced, are known to the compiler and are called +// indirectly by compiled object code. +// +// - For functions called only from assembly. +// +// - For functions called only from files whose build tags are not +// selected in the current build configuration. +// +// See https://github.com/golang/go/issues/71686 for discussion of +// these limitations. // // The unusedfunc algorithm is not as precise as the // golang.org/x/tools/cmd/deadcode tool, but it has the advantage that diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 8f101079a9c..629e45ff766 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -510,7 +510,7 @@ }, { "Name": "\"modernize\"", - "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq;", + "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq;\n\nTo apply all modernization fixes en masse, you can use the\nfollowing command:\n\n\t$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./...\n\nIf the tool warns of conflicting fixes, you may need to run it more\nthan once until it has applied all fixes cleanly. This command is\nnot an officially supported interface and may change in the future.", "Default": "true" }, { @@ -630,7 +630,7 @@ }, { "Name": "\"unusedfunc\"", - "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report a false positive for a declaration of an\nunexported function that is referenced from another package using\nthe go:linkname mechanism, if the declaration's doc comment does\nnot also have a go:linkname comment. (Such code is in any case\nstrongly discouraged: linkname annotations, if they must be used at\nall, should be used on both the declaration and the alias.)\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.", + "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report false positives in some situations, for\nexample:\n\n - For a declaration of an unexported function that is referenced\n from another package using the go:linkname mechanism, if the\n declaration's doc comment does not also have a go:linkname\n comment.\n\n (Such code is in any case strongly discouraged: linkname\n annotations, if they must be used at all, should be used on both\n the declaration and the alias.)\n\n - For compiler intrinsics in the \"runtime\" package that, though\n never referenced, are known to the compiler and are called\n indirectly by compiled object code.\n\n - For functions called only from assembly.\n\n - For functions called only from files whose build tags are not\n selected in the current build configuration.\n\nSee https://github.com/golang/go/issues/71686 for discussion of\nthese limitations.\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.", "Default": "true" }, { @@ -1189,7 +1189,7 @@ }, { "Name": "modernize", - "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq;", + "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq;\n\nTo apply all modernization fixes en masse, you can use the\nfollowing command:\n\n\t$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./...\n\nIf the tool warns of conflicting fixes, you may need to run it more\nthan once until it has applied all fixes cleanly. This command is\nnot an officially supported interface and may change in the future.", "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize", "Default": true }, @@ -1333,7 +1333,7 @@ }, { "Name": "unusedfunc", - "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report a false positive for a declaration of an\nunexported function that is referenced from another package using\nthe go:linkname mechanism, if the declaration's doc comment does\nnot also have a go:linkname comment. (Such code is in any case\nstrongly discouraged: linkname annotations, if they must be used at\nall, should be used on both the declaration and the alias.)\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.", + "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report false positives in some situations, for\nexample:\n\n - For a declaration of an unexported function that is referenced\n from another package using the go:linkname mechanism, if the\n declaration's doc comment does not also have a go:linkname\n comment.\n\n (Such code is in any case strongly discouraged: linkname\n annotations, if they must be used at all, should be used on both\n the declaration and the alias.)\n\n - For compiler intrinsics in the \"runtime\" package that, though\n never referenced, are known to the compiler and are called\n indirectly by compiled object code.\n\n - For functions called only from assembly.\n\n - For functions called only from files whose build tags are not\n selected in the current build configuration.\n\nSee https://github.com/golang/go/issues/71686 for discussion of\nthese limitations.\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.", "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedfunc", "Default": true }, From 8807101233fe3a3ccf31dca77298fe538436ee20 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Thu, 13 Feb 2025 17:48:26 -0500 Subject: [PATCH 23/99] gopls/internal/analysis/gofix: one function per pass Split the two passes into two separate functions. Declare a type to hold common state. No behavior changes. For golang/go#32816. Change-Id: I571956859b12687d824f36c80f75deb96db38d92 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649476 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/gofix/gofix.go | 224 +++++++++++++------------ 1 file changed, 120 insertions(+), 104 deletions(-) diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index 147399d315d..35d21c0e05a 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -37,63 +37,60 @@ var Analyzer = &analysis.Analyzer{ Requires: []*analysis.Analyzer{inspect.Analyzer}, } -func run(pass *analysis.Pass) (any, error) { - // Memoize repeated calls for same file. - fileContent := make(map[string][]byte) - readFile := func(node ast.Node) ([]byte, error) { - filename := pass.Fset.File(node.Pos()).Name() - content, ok := fileContent[filename] - if !ok { - var err error - content, err = pass.ReadFile(filename) - if err != nil { - return nil, err - } - fileContent[filename] = content - } - return content, nil - } +// analyzer holds the state for this analysis. +type analyzer struct { + pass *analysis.Pass + root cursor.Cursor + // memoization of repeated calls for same file. + fileContent map[string][]byte + // memoization of fact imports (nil => no fact) + inlinableFuncs map[*types.Func]*inline.Callee + inlinableConsts map[*types.Const]*goFixInlineConstFact + inlinableAliases map[*types.TypeName]*goFixInlineAliasFact +} - // Return the unique ast.File for a cursor. - currentFile := func(c cursor.Cursor) *ast.File { - cf, _ := moreiters.First(c.Ancestors((*ast.File)(nil))) - return cf.Node().(*ast.File) +func run(pass *analysis.Pass) (any, error) { + a := &analyzer{ + pass: pass, + root: cursor.Root(pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)), + fileContent: make(map[string][]byte), + inlinableFuncs: make(map[*types.Func]*inline.Callee), + inlinableConsts: make(map[*types.Const]*goFixInlineConstFact), + inlinableAliases: make(map[*types.TypeName]*goFixInlineAliasFact), } + a.find() + a.inline() + return nil, nil +} - // Pass 1: find functions and constants annotated with an appropriate "//go:fix" - // comment (the syntax proposed by #32816), - // and export a fact for each one. - var ( - inlinableFuncs = make(map[*types.Func]*inline.Callee) // memoization of fact import (nil => no fact) - inlinableConsts = make(map[*types.Const]*goFixInlineConstFact) - inlinableAliases = make(map[*types.TypeName]*goFixInlineAliasFact) - ) - - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - nodeFilter := []ast.Node{(*ast.FuncDecl)(nil), (*ast.GenDecl)(nil)} - inspect.Preorder(nodeFilter, func(n ast.Node) { - switch decl := n.(type) { +// find finds functions and constants annotated with an appropriate "//go:fix" +// comment (the syntax proposed by #32816), +// and exports a fact for each one. +func (a *analyzer) find() { + info := a.pass.TypesInfo + for cur := range a.root.Preorder((*ast.FuncDecl)(nil), (*ast.GenDecl)(nil)) { + switch decl := cur.Node().(type) { case *ast.FuncDecl: if !hasFixInline(decl.Doc) { - return + continue } - content, err := readFile(decl) + content, err := a.readFile(decl) if err != nil { - pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err) - return + a.pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err) + continue } - callee, err := inline.AnalyzeCallee(discard, pass.Fset, pass.Pkg, pass.TypesInfo, decl, content) + callee, err := inline.AnalyzeCallee(discard, a.pass.Fset, a.pass.Pkg, info, decl, content) if err != nil { - pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err) - return + a.pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err) + continue } - fn := pass.TypesInfo.Defs[decl.Name].(*types.Func) - pass.ExportObjectFact(fn, &goFixInlineFuncFact{callee}) - inlinableFuncs[fn] = callee + fn := info.Defs[decl.Name].(*types.Func) + a.pass.ExportObjectFact(fn, &goFixInlineFuncFact{callee}) + a.inlinableFuncs[fn] = callee case *ast.GenDecl: if decl.Tok != token.CONST && decl.Tok != token.TYPE { - return + continue } declInline := hasFixInline(decl.Doc) // Accept inline directives on the entire decl as well as individual specs. @@ -104,7 +101,7 @@ func run(pass *analysis.Pass) (any, error) { continue } if !spec.Assign.IsValid() { - pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: not a type alias") + a.pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: not a type alias") continue } if spec.TypeParams != nil { @@ -122,23 +119,23 @@ func run(pass *analysis.Pass) (any, error) { default: continue } - lhs := pass.TypesInfo.Defs[spec.Name].(*types.TypeName) + lhs := info.Defs[spec.Name].(*types.TypeName) // more (jba): test one alias pointing to another alias - rhs := pass.TypesInfo.Uses[rhsID].(*types.TypeName) + rhs := info.Uses[rhsID].(*types.TypeName) typ := &goFixInlineAliasFact{ RHSName: rhs.Name(), RHSPkgName: rhs.Pkg().Name(), RHSPkgPath: rhs.Pkg().Path(), } - if rhs.Pkg() == pass.Pkg { + if rhs.Pkg() == a.pass.Pkg { typ.rhsObj = rhs } - inlinableAliases[lhs] = typ + a.inlinableAliases[lhs] = typ // Create a fact only if the LHS is exported and defined at top level. // We create a fact even if the RHS is non-exported, // so we can warn about uses in other packages. if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { - pass.ExportObjectFact(lhs, typ) + a.pass.ExportObjectFact(lhs, typ) } case *ast.ValueSpec: // Tok == CONST @@ -154,58 +151,65 @@ func run(pass *analysis.Pass) (any, error) { switch e := val.(type) { case *ast.Ident: // Constants defined with the predeclared iota cannot be inlined. - if pass.TypesInfo.Uses[e] == builtinIota { - pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is iota") + if info.Uses[e] == builtinIota { + a.pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is iota") continue } rhsID = e case *ast.SelectorExpr: rhsID = e.Sel default: - pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is not the name of another constant") + a.pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is not the name of another constant") continue } - lhs := pass.TypesInfo.Defs[name].(*types.Const) - rhs := pass.TypesInfo.Uses[rhsID].(*types.Const) // must be so in a well-typed program + lhs := info.Defs[name].(*types.Const) + rhs := info.Uses[rhsID].(*types.Const) // must be so in a well-typed program con := &goFixInlineConstFact{ RHSName: rhs.Name(), RHSPkgName: rhs.Pkg().Name(), RHSPkgPath: rhs.Pkg().Path(), } - if rhs.Pkg() == pass.Pkg { + if rhs.Pkg() == a.pass.Pkg { con.rhsObj = rhs } - inlinableConsts[lhs] = con + a.inlinableConsts[lhs] = con // Create a fact only if the LHS is exported and defined at top level. // We create a fact even if the RHS is non-exported, // so we can warn about uses in other packages. if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { - pass.ExportObjectFact(lhs, con) + a.pass.ExportObjectFact(lhs, con) } } } } } } - }) + } +} + +// inline inlines each static call to an inlinable function +// and each reference to an inlinable constant or type alias. +// +// TODO(adonovan): handle multiple diffs that each add the same import. +func (a *analyzer) inline() { + // Return the unique ast.File for a cursor. + currentFile := func(c cursor.Cursor) *ast.File { + cf, _ := moreiters.First(c.Ancestors((*ast.File)(nil))) + return cf.Node().(*ast.File) + } - // Pass 2. Inline each static call to an inlinable function - // and each reference to an inlinable constant or type alias. - // - // TODO(adonovan): handle multiple diffs that each add the same import. - for cur := range cursor.Root(inspect).Preorder((*ast.CallExpr)(nil), (*ast.Ident)(nil)) { - n := cur.Node() - switch n := n.(type) { + for cur := range a.root.Preorder((*ast.CallExpr)(nil), (*ast.Ident)(nil)) { + switch n := cur.Node().(type) { case *ast.CallExpr: call := n - if fn := typeutil.StaticCallee(pass.TypesInfo, call); fn != nil { + if fn := typeutil.StaticCallee(a.pass.TypesInfo, call); fn != nil { // Inlinable? - callee, ok := inlinableFuncs[fn] + callee, ok := a.inlinableFuncs[fn] if !ok { var fact goFixInlineFuncFact - if pass.ImportObjectFact(fn, &fact) { + if a.pass.ImportObjectFact(fn, &fact) { callee = fact.Callee - inlinableFuncs[fn] = callee + a.inlinableFuncs[fn] = callee } } if callee == nil { @@ -213,23 +217,23 @@ func run(pass *analysis.Pass) (any, error) { } // Inline the call. - content, err := readFile(call) + content, err := a.readFile(call) if err != nil { - pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err) + a.pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err) continue } curFile := currentFile(cur) caller := &inline.Caller{ - Fset: pass.Fset, - Types: pass.Pkg, - Info: pass.TypesInfo, + Fset: a.pass.Fset, + Types: a.pass.Pkg, + Info: a.pass.TypesInfo, File: curFile, Call: call, Content: content, } res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard}) if err != nil { - pass.Reportf(call.Lparen, "%v", err) + a.pass.Reportf(call.Lparen, "%v", err) continue } if res.Literalized { @@ -253,7 +257,7 @@ func run(pass *analysis.Pass) (any, error) { NewText: []byte(edit.New), }) } - pass.Report(analysis.Diagnostic{ + a.pass.Report(analysis.Diagnostic{ Pos: call.Pos(), End: call.End(), Message: fmt.Sprintf("Call of %v should be inlined", callee), @@ -268,13 +272,13 @@ func run(pass *analysis.Pass) (any, error) { // If the identifier is a use of an inlinable type alias, suggest inlining it. // TODO(jba): much of this code is shared with the constant case, below. // Try to factor more of it out, unless it will change anyway when we move beyond simple RHS's. - if ali, ok := pass.TypesInfo.Uses[n].(*types.TypeName); ok { - inalias, ok := inlinableAliases[ali] + if ali, ok := a.pass.TypesInfo.Uses[n].(*types.TypeName); ok { + inalias, ok := a.inlinableAliases[ali] if !ok { var fact goFixInlineAliasFact - if pass.ImportObjectFact(ali, &fact) { + if a.pass.ImportObjectFact(ali, &fact) { inalias = &fact - inlinableAliases[ali] = inalias + a.inlinableAliases[ali] = inalias } } if inalias == nil { @@ -291,10 +295,10 @@ func run(pass *analysis.Pass) (any, error) { // If the RHS is not in the current package, AddImport will handle // shadowing, so we only need to worry about when both expressions // are in the current package. - if pass.Pkg.Path() == inalias.RHSPkgPath { + if a.pass.Pkg.Path() == inalias.RHSPkgPath { // fcon.rhsObj is the object referred to by B in the definition of A. - scope := pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope - _, obj := scope.LookupParent(inalias.RHSName, n.Pos()) // what "B" means in n's scope + scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope + _, obj := scope.LookupParent(inalias.RHSName, n.Pos()) // what "B" means in n's scope if obj == nil { // Should be impossible: if code at n can refer to the LHS, // it can refer to the RHS. @@ -304,7 +308,7 @@ func run(pass *analysis.Pass) (any, error) { // "B" means something different here than at the inlinable const's scope. continue } - } else if !analysisinternal.CanImport(pass.Pkg.Path(), inalias.RHSPkgPath) { + } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), inalias.RHSPkgPath) { // If this package can't see the RHS's package, we can't inline. continue } @@ -312,26 +316,26 @@ func run(pass *analysis.Pass) (any, error) { importPrefix string edits []analysis.TextEdit ) - if inalias.RHSPkgPath != pass.Pkg.Path() { + if inalias.RHSPkgPath != a.pass.Pkg.Path() { _, importPrefix, edits = analysisinternal.AddImport( - pass.TypesInfo, curFile, inalias.RHSPkgName, inalias.RHSPkgPath, inalias.RHSName, n.Pos()) + a.pass.TypesInfo, curFile, inalias.RHSPkgName, inalias.RHSPkgPath, inalias.RHSName, n.Pos()) } // If n is qualified by a package identifier, we'll need the full selector expression. var expr ast.Expr = n if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { expr = cur.Parent().Node().(ast.Expr) } - reportInline(pass, "type alias", "Type alias", expr, edits, importPrefix+inalias.RHSName) + a.reportInline("type alias", "Type alias", expr, edits, importPrefix+inalias.RHSName) continue } // If the identifier is a use of an inlinable constant, suggest inlining it. - if con, ok := pass.TypesInfo.Uses[n].(*types.Const); ok { - incon, ok := inlinableConsts[con] + if con, ok := a.pass.TypesInfo.Uses[n].(*types.Const); ok { + incon, ok := a.inlinableConsts[con] if !ok { var fact goFixInlineConstFact - if pass.ImportObjectFact(con, &fact) { + if a.pass.ImportObjectFact(con, &fact) { incon = &fact - inlinableConsts[con] = incon + a.inlinableConsts[con] = incon } } if incon == nil { @@ -350,10 +354,10 @@ func run(pass *analysis.Pass) (any, error) { // If the RHS is not in the current package, AddImport will handle // shadowing, so we only need to worry about when both expressions // are in the current package. - if pass.Pkg.Path() == incon.RHSPkgPath { + if a.pass.Pkg.Path() == incon.RHSPkgPath { // incon.rhsObj is the object referred to by B in the definition of A. - scope := pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope - _, obj := scope.LookupParent(incon.RHSName, n.Pos()) // what "B" means in n's scope + scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope + _, obj := scope.LookupParent(incon.RHSName, n.Pos()) // what "B" means in n's scope if obj == nil { // Should be impossible: if code at n can refer to the LHS, // it can refer to the RHS. @@ -363,7 +367,7 @@ func run(pass *analysis.Pass) (any, error) { // "B" means something different here than at the inlinable const's scope. continue } - } else if !analysisinternal.CanImport(pass.Pkg.Path(), incon.RHSPkgPath) { + } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), incon.RHSPkgPath) { // If this package can't see the RHS's package, we can't inline. continue } @@ -371,32 +375,30 @@ func run(pass *analysis.Pass) (any, error) { importPrefix string edits []analysis.TextEdit ) - if incon.RHSPkgPath != pass.Pkg.Path() { + if incon.RHSPkgPath != a.pass.Pkg.Path() { _, importPrefix, edits = analysisinternal.AddImport( - pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos()) + a.pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos()) } // If n is qualified by a package identifier, we'll need the full selector expression. var expr ast.Expr = n if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { expr = cur.Parent().Node().(ast.Expr) } - reportInline(pass, "constant", "Constant", expr, edits, importPrefix+incon.RHSName) + a.reportInline("constant", "Constant", expr, edits, importPrefix+incon.RHSName) } } } - - return nil, nil } // reportInline reports a diagnostic for fixing an inlinable name. -func reportInline(pass *analysis.Pass, kind, capKind string, ident ast.Expr, edits []analysis.TextEdit, newText string) { +func (a *analyzer) reportInline(kind, capKind string, ident ast.Expr, edits []analysis.TextEdit, newText string) { edits = append(edits, analysis.TextEdit{ Pos: ident.Pos(), End: ident.End(), NewText: []byte(newText), }) - name := analysisinternal.Format(pass.Fset, ident) - pass.Report(analysis.Diagnostic{ + name := analysisinternal.Format(a.pass.Fset, ident) + a.pass.Report(analysis.Diagnostic{ Pos: ident.Pos(), End: ident.End(), Message: fmt.Sprintf("%s %s should be inlined", capKind, name), @@ -407,6 +409,20 @@ func reportInline(pass *analysis.Pass, kind, capKind string, ident ast.Expr, edi }) } +func (a *analyzer) readFile(node ast.Node) ([]byte, error) { + filename := a.pass.Fset.File(node.Pos()).Name() + content, ok := a.fileContent[filename] + if !ok { + var err error + content, err = a.pass.ReadFile(filename) + if err != nil { + return nil, err + } + a.fileContent[filename] = content + } + return content, nil +} + // hasFixInline reports the presence of a "//go:fix inline" directive // in the comments. func hasFixInline(cg *ast.CommentGroup) bool { From 2880aae7521d3d90acfcef88c2efd17fc8f10369 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Thu, 13 Feb 2025 17:16:56 -0500 Subject: [PATCH 24/99] gopls/internal/protocol: Avoid omitempty for integer fields The existing code adds the json tag 'omitempty' to optional protocol fields, but for some integer-valued fields 0 and empty have different semantics. This CL changes the behavior so that optional integer-valued fields are not omitted if they are zero. FIxes golang.go/go#71489 Change-Id: I1f2cd6c6b0d6b7495adb1d8bc0b404ee9ea895f5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649455 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/protocol/generate/generate.go | 35 +++++++++++--------- gopls/internal/protocol/generate/output.go | 13 ++++++-- gopls/internal/protocol/tsprotocol.go | 22 ++++++------ 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/gopls/internal/protocol/generate/generate.go b/gopls/internal/protocol/generate/generate.go index 2bb14790940..9c7009113ab 100644 --- a/gopls/internal/protocol/generate/generate.go +++ b/gopls/internal/protocol/generate/generate.go @@ -54,39 +54,44 @@ func generateDoc(out *bytes.Buffer, doc string) { // decide if a property is optional, and if it needs a * // return ",omitempty" if it is optional, and "*" if it needs a pointer -func propStar(name string, t NameType, gotype string) (string, string) { - var opt, star string +func propStar(name string, t NameType, gotype string) (omitempty, indirect bool) { if t.Optional { - star = "*" - opt = ",omitempty" + switch gotype { + case "uint32", "int32": + // in FoldingRange.endLine, 0 and empty have different semantics + // There seem to be no other cases. + default: + indirect = true + omitempty = true + } } if strings.HasPrefix(gotype, "[]") || strings.HasPrefix(gotype, "map[") { - star = "" // passed by reference, so no need for * + indirect = false // passed by reference, so no need for * } else { switch gotype { - case "bool", "uint32", "int32", "string", "interface{}", "any": - star = "" // gopls compatibility if t.Optional + case "bool", "string", "interface{}", "any": + indirect = false // gopls compatibility if t.Optional } } - ostar, oopt := star, opt + oind, oomit := indirect, omitempty if newStar, ok := goplsStar[prop{name, t.Name}]; ok { switch newStar { case nothing: - star, opt = "", "" + indirect, omitempty = false, false case wantStar: - star, opt = "*", "" + indirect, omitempty = false, false case wantOpt: - star, opt = "", ",omitempty" + indirect, omitempty = false, true case wantOptStar: - star, opt = "*", ",omitempty" + indirect, omitempty = true, true } - if star == ostar && opt == oopt { // no change - log.Printf("goplsStar[ {%q, %q} ](%d) useless %s/%s %s/%s", name, t.Name, t.Line, ostar, star, oopt, opt) + if indirect == oind && omitempty == oomit { // no change + log.Printf("goplsStar[ {%q, %q} ](%d) useless %v/%v %v/%v", name, t.Name, t.Line, oind, indirect, oomit, omitempty) } usedGoplsStar[prop{name, t.Name}] = true } - return opt, star + return } func goName(s string) string { diff --git a/gopls/internal/protocol/generate/output.go b/gopls/internal/protocol/generate/output.go index ba9d0cb909f..5eaa0cba969 100644 --- a/gopls/internal/protocol/generate/output.go +++ b/gopls/internal/protocol/generate/output.go @@ -273,10 +273,17 @@ func genProps(out *bytes.Buffer, props []NameType, name string) { tp = newNm } // it's a pointer if it is optional, or for gopls compatibility - opt, star := propStar(name, p, tp) - json := fmt.Sprintf(" `json:\"%s%s\"`", p.Name, opt) + omit, star := propStar(name, p, tp) + json := fmt.Sprintf(" `json:\"%s\"`", p.Name) + if omit { + json = fmt.Sprintf(" `json:\"%s,omitempty\"`", p.Name) + } generateDoc(out, p.Documentation) - fmt.Fprintf(out, "\t%s %s%s %s\n", goName(p.Name), star, tp, json) + if star { + fmt.Fprintf(out, "\t%s *%s %s\n", goName(p.Name), tp, json) + } else { + fmt.Fprintf(out, "\t%s %s %s\n", goName(p.Name), tp, json) + } } } diff --git a/gopls/internal/protocol/tsprotocol.go b/gopls/internal/protocol/tsprotocol.go index 444e51e0717..7306f62a7ad 100644 --- a/gopls/internal/protocol/tsprotocol.go +++ b/gopls/internal/protocol/tsprotocol.go @@ -55,7 +55,7 @@ type ApplyWorkspaceEditResult struct { // Depending on the client's failure handling strategy `failedChange` might // contain the index of the change that failed. This property is only available // if the client signals a `failureHandlingStrategy` in its client capabilities. - FailedChange uint32 `json:"failedChange,omitempty"` + FailedChange uint32 `json:"failedChange"` } // A base for all symbol information. @@ -2377,12 +2377,12 @@ type FoldingRange struct { // To be valid, the end must be zero or larger and smaller than the number of lines in the document. StartLine uint32 `json:"startLine"` // The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. - StartCharacter uint32 `json:"startCharacter,omitempty"` + StartCharacter uint32 `json:"startCharacter"` // The zero-based end line of the range to fold. The folded area ends with the line's last character. // To be valid, the end must be zero or larger and smaller than the number of lines in the document. EndLine uint32 `json:"endLine"` // The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. - EndCharacter uint32 `json:"endCharacter,omitempty"` + EndCharacter uint32 `json:"endCharacter"` // Describes the kind of the folding range such as 'comment' or 'region'. The kind // is used to categorize folding ranges and used by commands like 'Fold all comments'. // See {@link FoldingRangeKind} for an enumeration of standardized kinds. @@ -2405,7 +2405,7 @@ type FoldingRangeClientCapabilities struct { // The maximum number of folding ranges that the client prefers to receive // per document. The value serves as a hint, servers are free to follow the // limit. - RangeLimit uint32 `json:"rangeLimit,omitempty"` + RangeLimit uint32 `json:"rangeLimit"` // If set, the client signals that it only supports folding complete lines. // If set, client will ignore specified `startCharacter` and `endCharacter` // properties in a FoldingRange. @@ -4148,7 +4148,7 @@ type PublishDiagnosticsParams struct { // Optional the version number of the document the diagnostics are published for. // // @since 3.15.0 - Version int32 `json:"version,omitempty"` + Version int32 `json:"version"` // An array of diagnostic information items. Diagnostics []Diagnostic `json:"diagnostics"` } @@ -4907,7 +4907,7 @@ type SignatureHelp struct { // // In future version of the protocol this property might become // mandatory to better express this. - ActiveSignature uint32 `json:"activeSignature,omitempty"` + ActiveSignature uint32 `json:"activeSignature"` // The active parameter of the active signature. // // If `null`, no parameter of the signature is active (for example a named @@ -4924,7 +4924,7 @@ type SignatureHelp struct { // In future version of the protocol this property might become // mandatory (but still nullable) to better express the active parameter if // the active signature does have any. - ActiveParameter uint32 `json:"activeParameter,omitempty"` + ActiveParameter uint32 `json:"activeParameter"` } // Client Capabilities for a {@link SignatureHelpRequest}. @@ -5036,7 +5036,7 @@ type SignatureInformation struct { // `SignatureHelp.activeParameter`. // // @since 3.16.0 - ActiveParameter uint32 `json:"activeParameter,omitempty"` + ActiveParameter uint32 `json:"activeParameter"` } // An interactive text edit. @@ -5261,7 +5261,7 @@ type TextDocumentContentChangePartial struct { // The optional length of the range that got replaced. // // @deprecated use range instead. - RangeLength uint32 `json:"rangeLength,omitempty"` + RangeLength uint32 `json:"rangeLength"` // The new text for the provided range. Text string `json:"text"` } @@ -5764,7 +5764,7 @@ type WorkDoneProgressBegin struct { // // The value should be steadily rising. Clients are free to ignore values // that are not following this rule. The value range is [0, 100]. - Percentage uint32 `json:"percentage,omitempty"` + Percentage uint32 `json:"percentage"` } // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#workDoneProgressCancelParams @@ -5824,7 +5824,7 @@ type WorkDoneProgressReport struct { // // The value should be steadily rising. Clients are free to ignore values // that are not following this rule. The value range is [0, 100] - Percentage uint32 `json:"percentage,omitempty"` + Percentage uint32 `json:"percentage"` } // Workspace specific client capabilities. From 32ffaa3103522a0b05609e241d1b03b3b1abb9a6 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Fri, 14 Feb 2025 08:03:39 -0500 Subject: [PATCH 25/99] gopls/internal/analysis/gofix: one function per kind Refactor so that each kind of inlinable (function, type alias, constant) has its own function for each pass. Refactoring only; no behavior changes. For golang/go#32816. Change-Id: I2f09b4020bcf03409664cee3b8379417d8717fa6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649456 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/analysis/gofix/gofix.go | 593 +++++++++++++------------ 1 file changed, 308 insertions(+), 285 deletions(-) diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index 35d21c0e05a..ffc64be755b 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -67,26 +67,10 @@ func run(pass *analysis.Pass) (any, error) { // comment (the syntax proposed by #32816), // and exports a fact for each one. func (a *analyzer) find() { - info := a.pass.TypesInfo for cur := range a.root.Preorder((*ast.FuncDecl)(nil), (*ast.GenDecl)(nil)) { switch decl := cur.Node().(type) { case *ast.FuncDecl: - if !hasFixInline(decl.Doc) { - continue - } - content, err := a.readFile(decl) - if err != nil { - a.pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err) - continue - } - callee, err := inline.AnalyzeCallee(discard, a.pass.Fset, a.pass.Pkg, info, decl, content) - if err != nil { - a.pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err) - continue - } - fn := info.Defs[decl.Name].(*types.Func) - a.pass.ExportObjectFact(fn, &goFixInlineFuncFact{callee}) - a.inlinableFuncs[fn] = callee + a.findFunc(decl) case *ast.GenDecl: if decl.Tok != token.CONST && decl.Tok != token.TYPE { @@ -97,91 +81,119 @@ func (a *analyzer) find() { for _, spec := range decl.Specs { switch spec := spec.(type) { case *ast.TypeSpec: // Tok == TYPE - if !declInline && !hasFixInline(spec.Doc) { - continue - } - if !spec.Assign.IsValid() { - a.pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: not a type alias") - continue - } - if spec.TypeParams != nil { - // TODO(jba): handle generic aliases - continue - } - // The alias must refer to another named type. - // TODO(jba): generalize to more type expressions. - var rhsID *ast.Ident - switch e := ast.Unparen(spec.Type).(type) { - case *ast.Ident: - rhsID = e - case *ast.SelectorExpr: - rhsID = e.Sel - default: - continue - } - lhs := info.Defs[spec.Name].(*types.TypeName) - // more (jba): test one alias pointing to another alias - rhs := info.Uses[rhsID].(*types.TypeName) - typ := &goFixInlineAliasFact{ - RHSName: rhs.Name(), - RHSPkgName: rhs.Pkg().Name(), - RHSPkgPath: rhs.Pkg().Path(), - } - if rhs.Pkg() == a.pass.Pkg { - typ.rhsObj = rhs - } - a.inlinableAliases[lhs] = typ - // Create a fact only if the LHS is exported and defined at top level. - // We create a fact even if the RHS is non-exported, - // so we can warn about uses in other packages. - if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { - a.pass.ExportObjectFact(lhs, typ) - } + a.findAlias(spec, declInline) case *ast.ValueSpec: // Tok == CONST - specInline := hasFixInline(spec.Doc) - if declInline || specInline { - for i, name := range spec.Names { - if i >= len(spec.Values) { - // Possible following an iota. - break - } - val := spec.Values[i] - var rhsID *ast.Ident - switch e := val.(type) { - case *ast.Ident: - // Constants defined with the predeclared iota cannot be inlined. - if info.Uses[e] == builtinIota { - a.pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is iota") - continue - } - rhsID = e - case *ast.SelectorExpr: - rhsID = e.Sel - default: - a.pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is not the name of another constant") - continue - } - lhs := info.Defs[name].(*types.Const) - rhs := info.Uses[rhsID].(*types.Const) // must be so in a well-typed program - con := &goFixInlineConstFact{ - RHSName: rhs.Name(), - RHSPkgName: rhs.Pkg().Name(), - RHSPkgPath: rhs.Pkg().Path(), - } - if rhs.Pkg() == a.pass.Pkg { - con.rhsObj = rhs - } - a.inlinableConsts[lhs] = con - // Create a fact only if the LHS is exported and defined at top level. - // We create a fact even if the RHS is non-exported, - // so we can warn about uses in other packages. - if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { - a.pass.ExportObjectFact(lhs, con) - } - } - } + a.findConst(spec, declInline) + } + } + } + } +} + +func (a *analyzer) findFunc(decl *ast.FuncDecl) { + if !hasFixInline(decl.Doc) { + return + } + content, err := a.readFile(decl) + if err != nil { + a.pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err) + return + } + callee, err := inline.AnalyzeCallee(discard, a.pass.Fset, a.pass.Pkg, a.pass.TypesInfo, decl, content) + if err != nil { + a.pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err) + return + } + fn := a.pass.TypesInfo.Defs[decl.Name].(*types.Func) + a.pass.ExportObjectFact(fn, &goFixInlineFuncFact{callee}) + a.inlinableFuncs[fn] = callee +} + +func (a *analyzer) findAlias(spec *ast.TypeSpec, declInline bool) { + if !declInline && !hasFixInline(spec.Doc) { + return + } + if !spec.Assign.IsValid() { + a.pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: not a type alias") + return + } + if spec.TypeParams != nil { + // TODO(jba): handle generic aliases + return + } + // The alias must refer to another named type. + // TODO(jba): generalize to more type expressions. + var rhsID *ast.Ident + switch e := ast.Unparen(spec.Type).(type) { + case *ast.Ident: + rhsID = e + case *ast.SelectorExpr: + rhsID = e.Sel + default: + return + } + lhs := a.pass.TypesInfo.Defs[spec.Name].(*types.TypeName) + // more (jba): test one alias pointing to another alias + rhs := a.pass.TypesInfo.Uses[rhsID].(*types.TypeName) + typ := &goFixInlineAliasFact{ + RHSName: rhs.Name(), + RHSPkgName: rhs.Pkg().Name(), + RHSPkgPath: rhs.Pkg().Path(), + } + if rhs.Pkg() == a.pass.Pkg { + typ.rhsObj = rhs + } + a.inlinableAliases[lhs] = typ + // Create a fact only if the LHS is exported and defined at top level. + // We create a fact even if the RHS is non-exported, + // so we can warn about uses in other packages. + if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { + a.pass.ExportObjectFact(lhs, typ) + } +} + +func (a *analyzer) findConst(spec *ast.ValueSpec, declInline bool) { + info := a.pass.TypesInfo + specInline := hasFixInline(spec.Doc) + if declInline || specInline { + for i, name := range spec.Names { + if i >= len(spec.Values) { + // Possible following an iota. + break + } + val := spec.Values[i] + var rhsID *ast.Ident + switch e := val.(type) { + case *ast.Ident: + // Constants defined with the predeclared iota cannot be inlined. + if info.Uses[e] == builtinIota { + a.pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is iota") + return } + rhsID = e + case *ast.SelectorExpr: + rhsID = e.Sel + default: + a.pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is not the name of another constant") + return + } + lhs := info.Defs[name].(*types.Const) + rhs := info.Uses[rhsID].(*types.Const) // must be so in a well-typed program + con := &goFixInlineConstFact{ + RHSName: rhs.Name(), + RHSPkgName: rhs.Pkg().Name(), + RHSPkgPath: rhs.Pkg().Path(), + } + if rhs.Pkg() == a.pass.Pkg { + con.rhsObj = rhs + } + a.inlinableConsts[lhs] = con + // Create a fact only if the LHS is exported and defined at top level. + // We create a fact even if the RHS is non-exported, + // so we can warn about uses in other packages. + if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { + a.pass.ExportObjectFact(lhs, con) } } } @@ -192,204 +204,209 @@ func (a *analyzer) find() { // // TODO(adonovan): handle multiple diffs that each add the same import. func (a *analyzer) inline() { - // Return the unique ast.File for a cursor. - currentFile := func(c cursor.Cursor) *ast.File { - cf, _ := moreiters.First(c.Ancestors((*ast.File)(nil))) - return cf.Node().(*ast.File) - } - for cur := range a.root.Preorder((*ast.CallExpr)(nil), (*ast.Ident)(nil)) { switch n := cur.Node().(type) { case *ast.CallExpr: - call := n - if fn := typeutil.StaticCallee(a.pass.TypesInfo, call); fn != nil { - // Inlinable? - callee, ok := a.inlinableFuncs[fn] - if !ok { - var fact goFixInlineFuncFact - if a.pass.ImportObjectFact(fn, &fact) { - callee = fact.Callee - a.inlinableFuncs[fn] = callee - } - } - if callee == nil { - continue // nope - } - - // 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) - continue - } - curFile := currentFile(cur) - caller := &inline.Caller{ - Fset: a.pass.Fset, - Types: a.pass.Pkg, - Info: a.pass.TypesInfo, - File: curFile, - Call: call, - Content: content, - } - res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard}) - if err != nil { - a.pass.Reportf(call.Lparen, "%v", err) - continue - } - 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.) - continue - } - 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), - }) - } - a.pass.Report(analysis.Diagnostic{ - Pos: call.Pos(), - End: call.End(), - Message: fmt.Sprintf("Call of %v should be inlined", callee), - SuggestedFixes: []analysis.SuggestedFix{{ - Message: fmt.Sprintf("Inline call of %v", callee), - TextEdits: textEdits, - }}, - }) - } + a.inlineCall(n, cur) case *ast.Ident: - // If the identifier is a use of an inlinable type alias, suggest inlining it. - // TODO(jba): much of this code is shared with the constant case, below. - // Try to factor more of it out, unless it will change anyway when we move beyond simple RHS's. - if ali, ok := a.pass.TypesInfo.Uses[n].(*types.TypeName); ok { - inalias, ok := a.inlinableAliases[ali] - if !ok { - var fact goFixInlineAliasFact - if a.pass.ImportObjectFact(ali, &fact) { - inalias = &fact - a.inlinableAliases[ali] = inalias - } - } - if inalias == nil { - continue // nope - } - curFile := currentFile(cur) - - // We have an identifier A here (n), possibly qualified by a package identifier (sel.X, - // where sel is the parent of X), // and an inlinable "type A = B" elsewhere (inali). - // Consider replacing A with B. - - // Check that the expression we are inlining (B) means the same thing - // (refers to the same object) in n's scope as it does in A's scope. - // If the RHS is not in the current package, AddImport will handle - // shadowing, so we only need to worry about when both expressions - // are in the current package. - if a.pass.Pkg.Path() == inalias.RHSPkgPath { - // fcon.rhsObj is the object referred to by B in the definition of A. - scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope - _, obj := scope.LookupParent(inalias.RHSName, n.Pos()) // what "B" means in n's scope - if obj == nil { - // Should be impossible: if code at n can refer to the LHS, - // it can refer to the RHS. - panic(fmt.Sprintf("no object for inlinable alias %s RHS %s", n.Name, inalias.RHSName)) - } - if obj != inalias.rhsObj { - // "B" means something different here than at the inlinable const's scope. - continue - } - } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), inalias.RHSPkgPath) { - // If this package can't see the RHS's package, we can't inline. - continue - } - var ( - importPrefix string - edits []analysis.TextEdit - ) - if inalias.RHSPkgPath != a.pass.Pkg.Path() { - _, importPrefix, edits = analysisinternal.AddImport( - a.pass.TypesInfo, curFile, inalias.RHSPkgName, inalias.RHSPkgPath, inalias.RHSName, n.Pos()) - } - // If n is qualified by a package identifier, we'll need the full selector expression. - var expr ast.Expr = n - if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { - expr = cur.Parent().Node().(ast.Expr) - } - a.reportInline("type alias", "Type alias", expr, edits, importPrefix+inalias.RHSName) - continue + switch t := a.pass.TypesInfo.Uses[n].(type) { + case *types.TypeName: + a.inlineAlias(t, cur) + case *types.Const: + a.inlineConst(t, cur) } - // If the identifier is a use of an inlinable constant, suggest inlining it. - if con, ok := a.pass.TypesInfo.Uses[n].(*types.Const); ok { - incon, ok := a.inlinableConsts[con] - if !ok { - var fact goFixInlineConstFact - if a.pass.ImportObjectFact(con, &fact) { - incon = &fact - a.inlinableConsts[con] = incon - } - } - if incon == nil { - continue // nope - } + } + } +} - // If n is qualified by a package identifier, we'll need the full selector expression. - curFile := currentFile(cur) - - // We have an identifier A here (n), possibly qualified by a package identifier (sel.X, - // where sel is the parent of n), // and an inlinable "const A = B" elsewhere (incon). - // Consider replacing A with B. - - // Check that the expression we are inlining (B) means the same thing - // (refers to the same object) in n's scope as it does in A's scope. - // If the RHS is not in the current package, AddImport will handle - // shadowing, so we only need to worry about when both expressions - // are in the current package. - if a.pass.Pkg.Path() == incon.RHSPkgPath { - // incon.rhsObj is the object referred to by B in the definition of A. - scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope - _, obj := scope.LookupParent(incon.RHSName, n.Pos()) // what "B" means in n's scope - if obj == nil { - // Should be impossible: if code at n can refer to the LHS, - // it can refer to the RHS. - panic(fmt.Sprintf("no object for inlinable const %s RHS %s", n.Name, incon.RHSName)) - } - if obj != incon.rhsObj { - // "B" means something different here than at the inlinable const's scope. - continue - } - } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), incon.RHSPkgPath) { - // If this package can't see the RHS's package, we can't inline. - continue - } - var ( - importPrefix string - edits []analysis.TextEdit - ) - if incon.RHSPkgPath != a.pass.Pkg.Path() { - _, importPrefix, edits = analysisinternal.AddImport( - a.pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos()) - } - // If n is qualified by a package identifier, we'll need the full selector expression. - var expr ast.Expr = n - if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { - expr = cur.Parent().Node().(ast.Expr) - } - a.reportInline("constant", "Constant", expr, edits, importPrefix+incon.RHSName) +// If call is a call to an inlinable func, suggest inlining its use at cur. +func (a *analyzer) inlineCall(call *ast.CallExpr, cur cursor.Cursor) { + if fn := typeutil.StaticCallee(a.pass.TypesInfo, call); fn != nil { + // Inlinable? + callee, ok := a.inlinableFuncs[fn] + if !ok { + var fact goFixInlineFuncFact + if a.pass.ImportObjectFact(fn, &fact) { + callee = fact.Callee + a.inlinableFuncs[fn] = callee } } + if callee == nil { + return // nope + } + + // 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 := currentFile(cur) + caller := &inline.Caller{ + Fset: a.pass.Fset, + Types: a.pass.Pkg, + Info: a.pass.TypesInfo, + File: curFile, + Call: call, + Content: content, + } + 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 + } + 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), + }) + } + a.pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Message: fmt.Sprintf("Call of %v should be inlined", callee), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Inline call of %v", callee), + TextEdits: textEdits, + }}, + }) } } +// If tn is the TypeName of an inlinable alias, suggest inlining its use at cur. +func (a *analyzer) inlineAlias(tn *types.TypeName, cur cursor.Cursor) { + inalias, ok := a.inlinableAliases[tn] + if !ok { + var fact goFixInlineAliasFact + if a.pass.ImportObjectFact(tn, &fact) { + inalias = &fact + a.inlinableAliases[tn] = inalias + } + } + if inalias == nil { + return // nope + } + curFile := currentFile(cur) + + // We have an identifier A here (n), possibly qualified by a package identifier (sel.X, + // where sel is the parent of X), // and an inlinable "type A = B" elsewhere (inali). + // Consider replacing A with B. + + // Check that the expression we are inlining (B) means the same thing + // (refers to the same object) in n's scope as it does in A's scope. + // If the RHS is not in the current package, AddImport will handle + // shadowing, so we only need to worry about when both expressions + // are in the current package. + n := cur.Node().(*ast.Ident) + if a.pass.Pkg.Path() == inalias.RHSPkgPath { + // fcon.rhsObj is the object referred to by B in the definition of A. + scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope + _, obj := scope.LookupParent(inalias.RHSName, n.Pos()) // what "B" means in n's scope + if obj == nil { + // Should be impossible: if code at n can refer to the LHS, + // it can refer to the RHS. + panic(fmt.Sprintf("no object for inlinable alias %s RHS %s", n.Name, inalias.RHSName)) + } + if obj != inalias.rhsObj { + // "B" means something different here than at the inlinable const's scope. + return + } + } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), inalias.RHSPkgPath) { + // If this package can't see the RHS's package, we can't inline. + return + } + var ( + importPrefix string + edits []analysis.TextEdit + ) + if inalias.RHSPkgPath != a.pass.Pkg.Path() { + _, importPrefix, edits = analysisinternal.AddImport( + a.pass.TypesInfo, curFile, inalias.RHSPkgName, inalias.RHSPkgPath, inalias.RHSName, n.Pos()) + } + // If n is qualified by a package identifier, we'll need the full selector expression. + var expr ast.Expr = n + if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { + expr = cur.Parent().Node().(ast.Expr) + } + a.reportInline("type alias", "Type alias", expr, edits, importPrefix+inalias.RHSName) +} + +// If con is an inlinable constant, suggest inlining its use at cur. +func (a *analyzer) inlineConst(con *types.Const, cur cursor.Cursor) { + incon, ok := a.inlinableConsts[con] + if !ok { + var fact goFixInlineConstFact + if a.pass.ImportObjectFact(con, &fact) { + incon = &fact + a.inlinableConsts[con] = incon + } + } + if incon == nil { + return // nope + } + + // If n is qualified by a package identifier, we'll need the full selector expression. + curFile := currentFile(cur) + n := cur.Node().(*ast.Ident) + + // We have an identifier A here (n), possibly qualified by a package identifier (sel.X, + // where sel is the parent of n), // and an inlinable "const A = B" elsewhere (incon). + // Consider replacing A with B. + + // Check that the expression we are inlining (B) means the same thing + // (refers to the same object) in n's scope as it does in A's scope. + // If the RHS is not in the current package, AddImport will handle + // shadowing, so we only need to worry about when both expressions + // are in the current package. + if a.pass.Pkg.Path() == incon.RHSPkgPath { + // incon.rhsObj is the object referred to by B in the definition of A. + scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope + _, obj := scope.LookupParent(incon.RHSName, n.Pos()) // what "B" means in n's scope + if obj == nil { + // Should be impossible: if code at n can refer to the LHS, + // it can refer to the RHS. + panic(fmt.Sprintf("no object for inlinable const %s RHS %s", n.Name, incon.RHSName)) + } + if obj != incon.rhsObj { + // "B" means something different here than at the inlinable const's scope. + return + } + } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), incon.RHSPkgPath) { + // If this package can't see the RHS's package, we can't inline. + return + } + var ( + importPrefix string + edits []analysis.TextEdit + ) + if incon.RHSPkgPath != a.pass.Pkg.Path() { + _, importPrefix, edits = analysisinternal.AddImport( + a.pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos()) + } + // If n is qualified by a package identifier, we'll need the full selector expression. + var expr ast.Expr = n + if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { + expr = cur.Parent().Node().(ast.Expr) + } + a.reportInline("constant", "Constant", expr, edits, importPrefix+incon.RHSName) +} + // reportInline reports a diagnostic for fixing an inlinable name. func (a *analyzer) reportInline(kind, capKind string, ident ast.Expr, edits []analysis.TextEdit, newText string) { edits = append(edits, analysis.TextEdit{ @@ -423,6 +440,12 @@ func (a *analyzer) readFile(node ast.Node) ([]byte, error) { return content, nil } +// currentFile returns the unique ast.File for a cursor. +func currentFile(c cursor.Cursor) *ast.File { + cf, _ := moreiters.First(c.Ancestors((*ast.File)(nil))) + return cf.Node().(*ast.File) +} + // hasFixInline reports the presence of a "//go:fix inline" directive // in the comments. func hasFixInline(cg *ast.CommentGroup) bool { From ead62e94e2a9fcb4562a875cad4b3308b2d616ac Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Sat, 15 Feb 2025 08:59:08 -0500 Subject: [PATCH 26/99] gopls/internal/analysis/modernize: handle parens In the maps modernizer, consider the possibility of parentheses surrounding some bits of syntax. Change-Id: I395de81b99f2e9b47dca7f4bbfbed66c0772b6f6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649975 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/analysis/modernize/maps.go | 4 ++-- .../testdata/src/mapsloop/mapsloop.go | 19 +++++++++++++++++++ .../testdata/src/mapsloop/mapsloop.go.golden | 10 ++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/gopls/internal/analysis/modernize/maps.go b/gopls/internal/analysis/modernize/maps.go index c93899621ef..91de659d107 100644 --- a/gopls/internal/analysis/modernize/maps.go +++ b/gopls/internal/analysis/modernize/maps.go @@ -87,9 +87,9 @@ func mapsloop(pass *analysis.Pass) { // Have: m = rhs; for k, v := range x { m[k] = v } var newMap bool rhs := assign.Rhs[0] - switch rhs := rhs.(type) { + switch rhs := ast.Unparen(rhs).(type) { case *ast.CallExpr: - if id, ok := rhs.Fun.(*ast.Ident); ok && + if id, ok := ast.Unparen(rhs.Fun).(*ast.Ident); ok && info.Uses[id] == builtinMake { // Have: m = make(...) newMap = true diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go index 769b4c84f60..e4e6963dbae 100644 --- a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go +++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go @@ -33,6 +33,25 @@ func useClone(src map[int]string) { for key, value := range src { dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" } + + dst = map[int]string{} + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + } + println(dst) +} + +func useCloneParen(src map[int]string) { + // Replace (make)(...) by maps.Clone. + dst := (make)(map[int]string, len(src)) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + } + + dst = (map[int]string{}) + for key, value := range src { + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + } println(dst) } diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden index b9aa39021e8..70b9a28ed5b 100644 --- a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden @@ -26,6 +26,16 @@ func useCopyGeneric[K comparable, V any, M ~map[K]V](dst, src M) { func useClone(src map[int]string) { // Replace make(...) by maps.Clone. dst := maps.Clone(src) + + dst = maps.Clone(src) + println(dst) +} + +func useCloneParen(src map[int]string) { + // Replace (make)(...) by maps.Clone. + dst := maps.Clone(src) + + dst = maps.Clone(src) println(dst) } From 94db7107945332a789135d458a0f0de1d7c00ddb Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Fri, 14 Feb 2025 21:17:29 +0000 Subject: [PATCH 27/99] all: upgrade go directive to at least 1.23.0 [generated] By now Go 1.24.0 has been released, and Go 1.22 is no longer supported per the Go Release Policy (https://go.dev/doc/devel/release#policy). For golang/go#69095. [git-generate] (cd . && go get go@1.23.0 && go mod tidy && go fix ./... && go mod edit -toolchain=none) (cd gopls/doc/assets && go get go@1.23.0 && go mod tidy && go fix ./... && go mod edit -toolchain=none) (cd gopls && echo 'skipping because it already has go1.23.4 >= go1.23.0, nothing to do') Change-Id: I37dad9abd1457a8a8aa940809b7ee6664fba006d Reviewed-on: https://go-review.googlesource.com/c/tools/+/649321 Auto-Submit: Gopher Robot LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov Reviewed-by: Cherry Mui Reviewed-by: Robert Findley --- cmd/callgraph/main_test.go | 1 - cmd/fiximports/main_test.go | 1 - cmd/godex/isAlias18.go | 1 - cmd/godex/isAlias19.go | 1 - cmd/goimports/goimports_gc.go | 1 - cmd/goimports/goimports_not_gc.go | 1 - cmd/gotype/sizesFor18.go | 1 - cmd/gotype/sizesFor19.go | 1 - cmd/splitdwarf/splitdwarf.go | 1 - cmd/stress/stress.go | 1 - go.mod | 2 +- go/analysis/multichecker/multichecker_test.go | 1 - go/analysis/passes/errorsas/errorsas_test.go | 1 - go/analysis/passes/stdversion/main.go | 1 - go/analysis/unitchecker/main.go | 1 - go/buildutil/allpackages_test.go | 1 - go/callgraph/cha/cha_test.go | 1 - go/callgraph/rta/rta_test.go | 1 - go/callgraph/vta/internal/trie/bits_test.go | 1 - go/callgraph/vta/internal/trie/trie_test.go | 1 - go/gcexportdata/example_test.go | 5 ----- go/gcexportdata/main.go | 1 - go/internal/gccgoimporter/newInterface10.go | 1 - go/internal/gccgoimporter/newInterface11.go | 1 - go/loader/loader_test.go | 1 - go/ssa/example_test.go | 3 --- go/ssa/ssautil/switch_test.go | 1 - go/ssa/stdlib_test.go | 1 - godoc/godoc17_test.go | 1 - godoc/static/makestatic.go | 1 - godoc/vfs/fs.go | 1 - gopls/doc/assets/go.mod | 2 +- internal/gcimporter/israce_test.go | 1 - internal/imports/mkindex.go | 1 - internal/jsonrpc2_v2/serve_go116.go | 1 - internal/jsonrpc2_v2/serve_pre116.go | 1 - internal/pprof/main.go | 1 - internal/robustio/copyfiles.go | 1 - internal/robustio/robustio_flaky.go | 1 - internal/robustio/robustio_other.go | 1 - internal/robustio/robustio_plan9.go | 1 - internal/robustio/robustio_posix.go | 1 - internal/stdlib/generate.go | 1 - internal/testenv/testenv_notunix.go | 1 - internal/testenv/testenv_unix.go | 1 - internal/typeparams/copytermlist.go | 1 - playground/socket/socket.go | 1 - refactor/eg/eg_test.go | 1 - refactor/importgraph/graph_test.go | 1 - 49 files changed, 2 insertions(+), 55 deletions(-) diff --git a/cmd/callgraph/main_test.go b/cmd/callgraph/main_test.go index ce634139e68..3b56cd7ffef 100644 --- a/cmd/callgraph/main_test.go +++ b/cmd/callgraph/main_test.go @@ -5,7 +5,6 @@ // No testdata on Android. //go:build !android && go1.11 -// +build !android,go1.11 package main diff --git a/cmd/fiximports/main_test.go b/cmd/fiximports/main_test.go index ebbd7520d2e..69f8726f135 100644 --- a/cmd/fiximports/main_test.go +++ b/cmd/fiximports/main_test.go @@ -5,7 +5,6 @@ // No testdata on Android. //go:build !android -// +build !android package main diff --git a/cmd/godex/isAlias18.go b/cmd/godex/isAlias18.go index 431602b2243..f1f78731d4c 100644 --- a/cmd/godex/isAlias18.go +++ b/cmd/godex/isAlias18.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.9 -// +build !go1.9 package main diff --git a/cmd/godex/isAlias19.go b/cmd/godex/isAlias19.go index e5889119fa1..db29555fd8c 100644 --- a/cmd/godex/isAlias19.go +++ b/cmd/godex/isAlias19.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.9 -// +build go1.9 package main diff --git a/cmd/goimports/goimports_gc.go b/cmd/goimports/goimports_gc.go index 3326646d035..3a88482fe8d 100644 --- a/cmd/goimports/goimports_gc.go +++ b/cmd/goimports/goimports_gc.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build gc -// +build gc package main diff --git a/cmd/goimports/goimports_not_gc.go b/cmd/goimports/goimports_not_gc.go index 344fe7576b0..21dc77920be 100644 --- a/cmd/goimports/goimports_not_gc.go +++ b/cmd/goimports/goimports_not_gc.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !gc -// +build !gc package main diff --git a/cmd/gotype/sizesFor18.go b/cmd/gotype/sizesFor18.go index 39e3d9f047e..15d2355ca42 100644 --- a/cmd/gotype/sizesFor18.go +++ b/cmd/gotype/sizesFor18.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.9 -// +build !go1.9 // This file contains a copy of the implementation of types.SizesFor // since this function is not available in go/types before Go 1.9. diff --git a/cmd/gotype/sizesFor19.go b/cmd/gotype/sizesFor19.go index 34181c8d04d..c46bb777024 100644 --- a/cmd/gotype/sizesFor19.go +++ b/cmd/gotype/sizesFor19.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.9 -// +build go1.9 package main diff --git a/cmd/splitdwarf/splitdwarf.go b/cmd/splitdwarf/splitdwarf.go index 90ff10b6a05..24aa239226c 100644 --- a/cmd/splitdwarf/splitdwarf.go +++ b/cmd/splitdwarf/splitdwarf.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd -// +build aix darwin dragonfly freebsd linux netbsd openbsd /* Splitdwarf uncompresses and copies the DWARF segment of a Mach-O diff --git a/cmd/stress/stress.go b/cmd/stress/stress.go index 6472064f933..e8b8641b387 100644 --- a/cmd/stress/stress.go +++ b/cmd/stress/stress.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows -// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris windows // The stress utility is intended for catching sporadic failures. // It runs a given process in parallel in a loop and collects any failures. diff --git a/go.mod b/go.mod index 8cea866daf8..9edfc58936d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module golang.org/x/tools -go 1.22.0 // => default GODEBUG has gotypesalias=0 +go 1.23.0 // => default GODEBUG has gotypesalias=0 require ( github.com/google/go-cmp v0.6.0 diff --git a/go/analysis/multichecker/multichecker_test.go b/go/analysis/multichecker/multichecker_test.go index 07bf977369b..94a280564ce 100644 --- a/go/analysis/multichecker/multichecker_test.go +++ b/go/analysis/multichecker/multichecker_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.12 -// +build go1.12 package multichecker_test diff --git a/go/analysis/passes/errorsas/errorsas_test.go b/go/analysis/passes/errorsas/errorsas_test.go index 6689d8114a7..5414f9e8b6d 100644 --- a/go/analysis/passes/errorsas/errorsas_test.go +++ b/go/analysis/passes/errorsas/errorsas_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.13 -// +build go1.13 package errorsas_test diff --git a/go/analysis/passes/stdversion/main.go b/go/analysis/passes/stdversion/main.go index 2156d41e4a9..bf1c3a0b31f 100644 --- a/go/analysis/passes/stdversion/main.go +++ b/go/analysis/passes/stdversion/main.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package main diff --git a/go/analysis/unitchecker/main.go b/go/analysis/unitchecker/main.go index 4374e7bf945..246be909249 100644 --- a/go/analysis/unitchecker/main.go +++ b/go/analysis/unitchecker/main.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // This file provides an example command for static checkers // conforming to the golang.org/x/tools/go/analysis API. diff --git a/go/buildutil/allpackages_test.go b/go/buildutil/allpackages_test.go index 6af86771104..2df5f27e223 100644 --- a/go/buildutil/allpackages_test.go +++ b/go/buildutil/allpackages_test.go @@ -5,7 +5,6 @@ // Incomplete source tree on Android. //go:build !android -// +build !android package buildutil_test diff --git a/go/callgraph/cha/cha_test.go b/go/callgraph/cha/cha_test.go index 5ac64e17244..7795cb44de0 100644 --- a/go/callgraph/cha/cha_test.go +++ b/go/callgraph/cha/cha_test.go @@ -5,7 +5,6 @@ // No testdata on Android. //go:build !android -// +build !android package cha_test diff --git a/go/callgraph/rta/rta_test.go b/go/callgraph/rta/rta_test.go index 74e77b01291..dcec98d7c5d 100644 --- a/go/callgraph/rta/rta_test.go +++ b/go/callgraph/rta/rta_test.go @@ -5,7 +5,6 @@ // No testdata on Android. //go:build !android -// +build !android package rta_test diff --git a/go/callgraph/vta/internal/trie/bits_test.go b/go/callgraph/vta/internal/trie/bits_test.go index 07784cdffac..f6e510eccd0 100644 --- a/go/callgraph/vta/internal/trie/bits_test.go +++ b/go/callgraph/vta/internal/trie/bits_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.13 -// +build go1.13 package trie diff --git a/go/callgraph/vta/internal/trie/trie_test.go b/go/callgraph/vta/internal/trie/trie_test.go index c0651b0ef86..71fd398f12c 100644 --- a/go/callgraph/vta/internal/trie/trie_test.go +++ b/go/callgraph/vta/internal/trie/trie_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.13 -// +build go1.13 package trie diff --git a/go/gcexportdata/example_test.go b/go/gcexportdata/example_test.go index 9574f30d32b..852ba5a597c 100644 --- a/go/gcexportdata/example_test.go +++ b/go/gcexportdata/example_test.go @@ -3,11 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.7 && gc && !android && !ios && (unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || plan9 || windows) -// +build go1.7 -// +build gc -// +build !android -// +build !ios -// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris plan9 windows package gcexportdata_test diff --git a/go/gcexportdata/main.go b/go/gcexportdata/main.go index e9df4e9a9a5..0b267e33867 100644 --- a/go/gcexportdata/main.go +++ b/go/gcexportdata/main.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // The gcexportdata command is a diagnostic tool that displays the // contents of gc export data files. diff --git a/go/internal/gccgoimporter/newInterface10.go b/go/internal/gccgoimporter/newInterface10.go index 1b449ef9886..f49c9b067dd 100644 --- a/go/internal/gccgoimporter/newInterface10.go +++ b/go/internal/gccgoimporter/newInterface10.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.11 -// +build !go1.11 package gccgoimporter diff --git a/go/internal/gccgoimporter/newInterface11.go b/go/internal/gccgoimporter/newInterface11.go index 631546ec66f..c7d5edb4858 100644 --- a/go/internal/gccgoimporter/newInterface11.go +++ b/go/internal/gccgoimporter/newInterface11.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.11 -// +build go1.11 package gccgoimporter diff --git a/go/loader/loader_test.go b/go/loader/loader_test.go index 2276b49ad6f..eb9feb221f0 100644 --- a/go/loader/loader_test.go +++ b/go/loader/loader_test.go @@ -5,7 +5,6 @@ // No testdata on Android. //go:build !android -// +build !android package loader_test diff --git a/go/ssa/example_test.go b/go/ssa/example_test.go index e0fba0be681..03775414df2 100644 --- a/go/ssa/example_test.go +++ b/go/ssa/example_test.go @@ -3,9 +3,6 @@ // license that can be found in the LICENSE file. //go:build !android && !ios && (unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || plan9 || windows) -// +build !android -// +build !ios -// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris plan9 windows package ssa_test diff --git a/go/ssa/ssautil/switch_test.go b/go/ssa/ssautil/switch_test.go index 081b09010ee..6ff5c9b92c3 100644 --- a/go/ssa/ssautil/switch_test.go +++ b/go/ssa/ssautil/switch_test.go @@ -5,7 +5,6 @@ // No testdata on Android. //go:build !android -// +build !android package ssautil_test diff --git a/go/ssa/stdlib_test.go b/go/ssa/stdlib_test.go index 9b78cfbf839..08df50b9eeb 100644 --- a/go/ssa/stdlib_test.go +++ b/go/ssa/stdlib_test.go @@ -5,7 +5,6 @@ // Incomplete source tree on Android. //go:build !android -// +build !android package ssa_test diff --git a/godoc/godoc17_test.go b/godoc/godoc17_test.go index 82e23e64775..c8bf2d96d42 100644 --- a/godoc/godoc17_test.go +++ b/godoc/godoc17_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.7 -// +build go1.7 package godoc diff --git a/godoc/static/makestatic.go b/godoc/static/makestatic.go index a8a652f8ed5..5a7337290ff 100644 --- a/godoc/static/makestatic.go +++ b/godoc/static/makestatic.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // Command makestatic writes the generated file buffer to "static.go". // It is intended to be invoked via "go generate" (directive in "gen.go"). diff --git a/godoc/vfs/fs.go b/godoc/vfs/fs.go index f12d653fef2..2bec5886052 100644 --- a/godoc/vfs/fs.go +++ b/godoc/vfs/fs.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.16 -// +build go1.16 package vfs diff --git a/gopls/doc/assets/go.mod b/gopls/doc/assets/go.mod index 73f49695726..9b417f19ed8 100644 --- a/gopls/doc/assets/go.mod +++ b/gopls/doc/assets/go.mod @@ -4,4 +4,4 @@ module golang.org/x/tools/gopls/doc/assets -go 1.19 +go 1.23.0 diff --git a/internal/gcimporter/israce_test.go b/internal/gcimporter/israce_test.go index 885ba1c01c5..c75a16b7a1b 100644 --- a/internal/gcimporter/israce_test.go +++ b/internal/gcimporter/israce_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build race -// +build race package gcimporter_test diff --git a/internal/imports/mkindex.go b/internal/imports/mkindex.go index ff006b0cd2e..10e8da5243d 100644 --- a/internal/imports/mkindex.go +++ b/internal/imports/mkindex.go @@ -1,5 +1,4 @@ //go:build ignore -// +build ignore // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style diff --git a/internal/jsonrpc2_v2/serve_go116.go b/internal/jsonrpc2_v2/serve_go116.go index 2dac7413f31..19114502d1c 100644 --- a/internal/jsonrpc2_v2/serve_go116.go +++ b/internal/jsonrpc2_v2/serve_go116.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.16 -// +build go1.16 package jsonrpc2 diff --git a/internal/jsonrpc2_v2/serve_pre116.go b/internal/jsonrpc2_v2/serve_pre116.go index ef5477fecb9..9e8ece2ea7b 100644 --- a/internal/jsonrpc2_v2/serve_pre116.go +++ b/internal/jsonrpc2_v2/serve_pre116.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.16 -// +build !go1.16 package jsonrpc2 diff --git a/internal/pprof/main.go b/internal/pprof/main.go index 5e1ae633b4d..42aa187a6a7 100644 --- a/internal/pprof/main.go +++ b/internal/pprof/main.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // The pprof command prints the total time in a pprof profile provided // through the standard input. diff --git a/internal/robustio/copyfiles.go b/internal/robustio/copyfiles.go index 8c93fcd7163..8aace49da8b 100644 --- a/internal/robustio/copyfiles.go +++ b/internal/robustio/copyfiles.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // The copyfiles script copies the contents of the internal cmd/go robustio // package to the current directory, with adjustments to make it build. diff --git a/internal/robustio/robustio_flaky.go b/internal/robustio/robustio_flaky.go index d5c241857b4..c56e36ca624 100644 --- a/internal/robustio/robustio_flaky.go +++ b/internal/robustio/robustio_flaky.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build windows || darwin -// +build windows darwin package robustio diff --git a/internal/robustio/robustio_other.go b/internal/robustio/robustio_other.go index 3a20cac6cf8..da9a46e4fac 100644 --- a/internal/robustio/robustio_other.go +++ b/internal/robustio/robustio_other.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !windows && !darwin -// +build !windows,!darwin package robustio diff --git a/internal/robustio/robustio_plan9.go b/internal/robustio/robustio_plan9.go index 9fa4cacb5a3..3026b9f6321 100644 --- a/internal/robustio/robustio_plan9.go +++ b/internal/robustio/robustio_plan9.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build plan9 -// +build plan9 package robustio diff --git a/internal/robustio/robustio_posix.go b/internal/robustio/robustio_posix.go index cf74865d0b5..6b4beec96fc 100644 --- a/internal/robustio/robustio_posix.go +++ b/internal/robustio/robustio_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !windows && !plan9 -// +build !windows,!plan9 package robustio diff --git a/internal/stdlib/generate.go b/internal/stdlib/generate.go index d4964f60955..1192885405c 100644 --- a/internal/stdlib/generate.go +++ b/internal/stdlib/generate.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // The generate command reads all the GOROOT/api/go1.*.txt files and // generates a single combined manifest.go file containing the Go diff --git a/internal/testenv/testenv_notunix.go b/internal/testenv/testenv_notunix.go index e9ce0d3649d..85b3820e3fb 100644 --- a/internal/testenv/testenv_notunix.go +++ b/internal/testenv/testenv_notunix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !(unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris) -// +build !unix,!aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris package testenv diff --git a/internal/testenv/testenv_unix.go b/internal/testenv/testenv_unix.go index bc6af1ff81d..d635b96b31b 100644 --- a/internal/testenv/testenv_unix.go +++ b/internal/testenv/testenv_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris package testenv diff --git a/internal/typeparams/copytermlist.go b/internal/typeparams/copytermlist.go index 5357f9d2fd7..1edaaa01c9a 100644 --- a/internal/typeparams/copytermlist.go +++ b/internal/typeparams/copytermlist.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // copytermlist.go copies the term list algorithm from GOROOT/src/go/types. diff --git a/playground/socket/socket.go b/playground/socket/socket.go index 9e5b4a954d2..378edd4c3a5 100644 --- a/playground/socket/socket.go +++ b/playground/socket/socket.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !appengine -// +build !appengine // Package socket implements a WebSocket-based playground backend. // Clients connect to a websocket handler and send run/kill commands, and diff --git a/refactor/eg/eg_test.go b/refactor/eg/eg_test.go index eb54f0b3f95..4dc24f53358 100644 --- a/refactor/eg/eg_test.go +++ b/refactor/eg/eg_test.go @@ -5,7 +5,6 @@ // No testdata on Android. //go:build !android -// +build !android package eg_test diff --git a/refactor/importgraph/graph_test.go b/refactor/importgraph/graph_test.go index f3378a41e86..a07cc633454 100644 --- a/refactor/importgraph/graph_test.go +++ b/refactor/importgraph/graph_test.go @@ -5,7 +5,6 @@ // Incomplete std lib sources on Android. //go:build !android -// +build !android package importgraph_test From c18bffa1b03c9346f0dd1af830b09979e63c7b5f Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Sun, 16 Feb 2025 02:16:29 -0500 Subject: [PATCH 28/99] all: delete redundant //go:debug gotypesalias=1 directives [generated] The module's go directive is >= 1.23 now, so these files no longer have any effect. Also drop the outdated comment in go.mod. For golang/go#69772. [git-generate] find . -type f -name gotypesalias.go -exec rm {} + sed -i '' 's| // => default GODEBUG has gotypesalias=0||' go.mod Change-Id: Id0fa54f89695991de6c8721aadecfd587826d158 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650095 Reviewed-by: Cherry Mui Auto-Submit: Dmitri Shuralyov Reviewed-by: Funda Secgin Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- cmd/bundle/gotypesalias.go | 12 ------------ cmd/callgraph/gotypesalias.go | 12 ------------ cmd/deadcode/gotypesalias.go | 12 ------------ cmd/eg/gotypesalias.go | 12 ------------ cmd/godex/gotypesalias.go | 12 ------------ cmd/godoc/gotypesalias.go | 12 ------------ cmd/goimports/gotypesalias.go | 12 ------------ cmd/gomvpkg/gotypesalias.go | 12 ------------ cmd/gotype/gotypesalias.go | 12 ------------ cmd/ssadump/gotypesalias.go | 12 ------------ cmd/stringer/gotypesalias.go | 12 ------------ go.mod | 2 +- go/analysis/passes/defers/cmd/defers/gotypesalias.go | 12 ------------ .../cmd/fieldalignment/gotypesalias.go | 12 ------------ .../passes/findcall/cmd/findcall/gotypesalias.go | 12 ------------ .../passes/httpmux/cmd/httpmux/gotypesalias.go | 12 ------------ .../ifaceassert/cmd/ifaceassert/gotypesalias.go | 12 ------------ .../passes/lostcancel/cmd/lostcancel/gotypesalias.go | 12 ------------ .../passes/nilness/cmd/nilness/gotypesalias.go | 12 ------------ go/analysis/passes/shadow/cmd/shadow/gotypesalias.go | 12 ------------ .../stringintconv/cmd/stringintconv/gotypesalias.go | 12 ------------ .../passes/unmarshal/cmd/unmarshal/gotypesalias.go | 12 ------------ .../unusedresult/cmd/unusedresult/gotypesalias.go | 12 ------------ go/packages/gopackages/gotypesalias.go | 12 ------------ go/packages/internal/nodecount/gotypesalias.go | 12 ------------ go/types/internal/play/gotypesalias.go | 12 ------------ 26 files changed, 1 insertion(+), 301 deletions(-) delete mode 100644 cmd/bundle/gotypesalias.go delete mode 100644 cmd/callgraph/gotypesalias.go delete mode 100644 cmd/deadcode/gotypesalias.go delete mode 100644 cmd/eg/gotypesalias.go delete mode 100644 cmd/godex/gotypesalias.go delete mode 100644 cmd/godoc/gotypesalias.go delete mode 100644 cmd/goimports/gotypesalias.go delete mode 100644 cmd/gomvpkg/gotypesalias.go delete mode 100644 cmd/gotype/gotypesalias.go delete mode 100644 cmd/ssadump/gotypesalias.go delete mode 100644 cmd/stringer/gotypesalias.go delete mode 100644 go/analysis/passes/defers/cmd/defers/gotypesalias.go delete mode 100644 go/analysis/passes/fieldalignment/cmd/fieldalignment/gotypesalias.go delete mode 100644 go/analysis/passes/findcall/cmd/findcall/gotypesalias.go delete mode 100644 go/analysis/passes/httpmux/cmd/httpmux/gotypesalias.go delete mode 100644 go/analysis/passes/ifaceassert/cmd/ifaceassert/gotypesalias.go delete mode 100644 go/analysis/passes/lostcancel/cmd/lostcancel/gotypesalias.go delete mode 100644 go/analysis/passes/nilness/cmd/nilness/gotypesalias.go delete mode 100644 go/analysis/passes/shadow/cmd/shadow/gotypesalias.go delete mode 100644 go/analysis/passes/stringintconv/cmd/stringintconv/gotypesalias.go delete mode 100644 go/analysis/passes/unmarshal/cmd/unmarshal/gotypesalias.go delete mode 100644 go/analysis/passes/unusedresult/cmd/unusedresult/gotypesalias.go delete mode 100644 go/packages/gopackages/gotypesalias.go delete mode 100644 go/packages/internal/nodecount/gotypesalias.go delete mode 100644 go/types/internal/play/gotypesalias.go diff --git a/cmd/bundle/gotypesalias.go b/cmd/bundle/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/bundle/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/cmd/callgraph/gotypesalias.go b/cmd/callgraph/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/callgraph/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/cmd/deadcode/gotypesalias.go b/cmd/deadcode/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/deadcode/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/cmd/eg/gotypesalias.go b/cmd/eg/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/eg/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/cmd/godex/gotypesalias.go b/cmd/godex/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/godex/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/cmd/godoc/gotypesalias.go b/cmd/godoc/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/godoc/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/cmd/goimports/gotypesalias.go b/cmd/goimports/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/goimports/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/cmd/gomvpkg/gotypesalias.go b/cmd/gomvpkg/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/gomvpkg/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/cmd/gotype/gotypesalias.go b/cmd/gotype/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/gotype/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/cmd/ssadump/gotypesalias.go b/cmd/ssadump/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/ssadump/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/cmd/stringer/gotypesalias.go b/cmd/stringer/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/cmd/stringer/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go.mod b/go.mod index 9edfc58936d..bc7636b4cf8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module golang.org/x/tools -go 1.23.0 // => default GODEBUG has gotypesalias=0 +go 1.23.0 require ( github.com/google/go-cmp v0.6.0 diff --git a/go/analysis/passes/defers/cmd/defers/gotypesalias.go b/go/analysis/passes/defers/cmd/defers/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/defers/cmd/defers/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/analysis/passes/fieldalignment/cmd/fieldalignment/gotypesalias.go b/go/analysis/passes/fieldalignment/cmd/fieldalignment/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/fieldalignment/cmd/fieldalignment/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/analysis/passes/findcall/cmd/findcall/gotypesalias.go b/go/analysis/passes/findcall/cmd/findcall/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/findcall/cmd/findcall/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/analysis/passes/httpmux/cmd/httpmux/gotypesalias.go b/go/analysis/passes/httpmux/cmd/httpmux/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/httpmux/cmd/httpmux/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/analysis/passes/ifaceassert/cmd/ifaceassert/gotypesalias.go b/go/analysis/passes/ifaceassert/cmd/ifaceassert/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/ifaceassert/cmd/ifaceassert/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/analysis/passes/lostcancel/cmd/lostcancel/gotypesalias.go b/go/analysis/passes/lostcancel/cmd/lostcancel/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/lostcancel/cmd/lostcancel/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/analysis/passes/nilness/cmd/nilness/gotypesalias.go b/go/analysis/passes/nilness/cmd/nilness/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/nilness/cmd/nilness/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/analysis/passes/shadow/cmd/shadow/gotypesalias.go b/go/analysis/passes/shadow/cmd/shadow/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/shadow/cmd/shadow/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/analysis/passes/stringintconv/cmd/stringintconv/gotypesalias.go b/go/analysis/passes/stringintconv/cmd/stringintconv/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/stringintconv/cmd/stringintconv/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/analysis/passes/unmarshal/cmd/unmarshal/gotypesalias.go b/go/analysis/passes/unmarshal/cmd/unmarshal/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/unmarshal/cmd/unmarshal/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/analysis/passes/unusedresult/cmd/unusedresult/gotypesalias.go b/go/analysis/passes/unusedresult/cmd/unusedresult/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/analysis/passes/unusedresult/cmd/unusedresult/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/packages/gopackages/gotypesalias.go b/go/packages/gopackages/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/packages/gopackages/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/packages/internal/nodecount/gotypesalias.go b/go/packages/internal/nodecount/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/packages/internal/nodecount/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). diff --git a/go/types/internal/play/gotypesalias.go b/go/types/internal/play/gotypesalias.go deleted file mode 100644 index 288c10c2d0a..00000000000 --- a/go/types/internal/play/gotypesalias.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.23 - -//go:debug gotypesalias=1 - -package main - -// Materialize aliases whenever the go toolchain version is after 1.23 (#69772). -// Remove this file after go.mod >= 1.23 (which implies gotypesalias=1). From d115b345e2022d300181300e01d379c4f65da9f6 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 17 Feb 2025 14:24:33 -0500 Subject: [PATCH 29/99] gopls/internal/analysis: simplify type-error analyzers with Cursor This CL makes a number of simplifications to the type-error analyzers: - Nodes are found using Cursor.FindPos from the error position, which is very fast; - Error position information is read from types.Error instead of formatting the ast.File (!) then invoking the dubious heuristics of analysisinternal.TypeErrorEndPos, which scans the text (!!) assuming well-formattedness (!!!). - plus various minor cleanups. - rename typesinternal.ReadGo116ErrorData to ErrorCodeStartEnd. Updates golang/go#65966 Updates golang/go#71803 Change-Id: Ie4561144040b001b957ef6a3c3631328297d5001 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650217 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam Auto-Submit: Alan Donovan Commit-Queue: Alan Donovan --- .../analysis/fillreturns/fillreturns.go | 147 +++++++----------- .../internal/analysis/nonewvars/nonewvars.go | 81 +++++----- .../analysis/noresultvalues/noresultvalues.go | 61 +++----- gopls/internal/cache/check.go | 5 +- gopls/internal/golang/codeaction.go | 2 +- .../marker/testdata/highlight/controlflow.txt | 3 +- internal/analysisinternal/analysis.go | 2 + internal/typesinternal/types.go | 6 +- 8 files changed, 130 insertions(+), 177 deletions(-) diff --git a/gopls/internal/analysis/fillreturns/fillreturns.go b/gopls/internal/analysis/fillreturns/fillreturns.go index 8a602dc2eef..b6bcc1f24dc 100644 --- a/gopls/internal/analysis/fillreturns/fillreturns.go +++ b/gopls/internal/analysis/fillreturns/fillreturns.go @@ -15,9 +15,12 @@ import ( "strings" "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/fuzzy" + "golang.org/x/tools/gopls/internal/util/moreiters" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/typesinternal" ) @@ -27,105 +30,41 @@ var doc string var Analyzer = &analysis.Analyzer{ Name: "fillreturns", Doc: analysisinternal.MustExtractDoc(doc, "fillreturns"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: run, RunDespiteErrors: true, URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillreturns", } func run(pass *analysis.Pass) (interface{}, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) info := pass.TypesInfo - if info == nil { - return nil, fmt.Errorf("nil TypeInfo") - } outer: for _, typeErr := range pass.TypeErrors { - // Filter out the errors that are not relevant to this analyzer. - if !FixesError(typeErr) { - continue - } - var file *ast.File - for _, f := range pass.Files { - if f.FileStart <= typeErr.Pos && typeErr.Pos <= f.FileEnd { - file = f - break - } - } - if file == nil { - continue - } - - // Get the end position of the error. - // (This heuristic assumes that the buffer is formatted, - // at least up to the end position of the error.) - var buf bytes.Buffer - if err := format.Node(&buf, pass.Fset, file); err != nil { - continue + if !fixesError(typeErr) { + continue // irrelevant type error } - typeErrEndPos := analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), typeErr.Pos) - - // TODO(rfindley): much of the error handling code below returns, when it - // should probably continue. - - // Get the path for the relevant range. - path, _ := astutil.PathEnclosingInterval(file, typeErr.Pos, typeErrEndPos) - if len(path) == 0 { - return nil, nil - } - - // Find the enclosing return statement. - var ret *ast.ReturnStmt - var retIdx int - for i, n := range path { - if r, ok := n.(*ast.ReturnStmt); ok { - ret = r - retIdx = i - break - } + _, start, end, ok := typesinternal.ErrorCodeStartEnd(typeErr) + if !ok { + continue // no position information } - if ret == nil { - return nil, nil + curErr, ok := cursor.Root(inspect).FindPos(start, end) + if !ok { + continue // can't find node } - // Get the function type that encloses the ReturnStmt. - var enclosingFunc *ast.FuncType - for _, n := range path[retIdx+1:] { - switch node := n.(type) { - case *ast.FuncLit: - enclosingFunc = node.Type - case *ast.FuncDecl: - enclosingFunc = node.Type - } - if enclosingFunc != nil { - break - } - } - if enclosingFunc == nil || enclosingFunc.Results == nil { - continue - } - - // Skip any generic enclosing functions, since type parameters don't - // have 0 values. - // TODO(rfindley): We should be able to handle this if the return - // values are all concrete types. - if tparams := enclosingFunc.TypeParams; tparams != nil && tparams.NumFields() > 0 { - return nil, nil - } - - // Find the function declaration that encloses the ReturnStmt. - var outer *ast.FuncDecl - for _, p := range path { - if p, ok := p.(*ast.FuncDecl); ok { - outer = p - break + // Find cursor for enclosing return statement (which may be curErr itself). + curRet := curErr + if _, ok := curRet.Node().(*ast.ReturnStmt); !ok { + curRet, ok = moreiters.First(curErr.Ancestors((*ast.ReturnStmt)(nil))) + if !ok { + continue // no enclosing return } } - if outer == nil { - return nil, nil - } + ret := curRet.Node().(*ast.ReturnStmt) - // Skip any return statements that contain function calls with multiple - // return values. + // Skip if any return argument is a tuple-valued function call. for _, expr := range ret.Results { e, ok := expr.(*ast.CallExpr) if !ok { @@ -136,24 +75,47 @@ outer: } } + // Get type of innermost enclosing function. + var funcType *ast.FuncType + curFunc, _ := enclosingFunc(curRet) // can't fail + switch fn := curFunc.Node().(type) { + case *ast.FuncLit: + funcType = fn.Type + case *ast.FuncDecl: + funcType = fn.Type + + // Skip generic functions since type parameters don't have zero values. + // TODO(rfindley): We should be able to handle this if the return + // values are all concrete types. + if funcType.TypeParams.NumFields() > 0 { + continue + } + } + if funcType.Results == nil { + continue + } + // Duplicate the return values to track which values have been matched. remaining := make([]ast.Expr, len(ret.Results)) copy(remaining, ret.Results) - fixed := make([]ast.Expr, len(enclosingFunc.Results.List)) + fixed := make([]ast.Expr, len(funcType.Results.List)) // For each value in the return function declaration, find the leftmost element // in the return statement that has the desired type. If no such element exists, // fill in the missing value with the appropriate "zero" value. // Beware that type information may be incomplete. var retTyps []types.Type - for _, ret := range enclosingFunc.Results.List { + for _, ret := range funcType.Results.List { retTyp := info.TypeOf(ret.Type) if retTyp == nil { return nil, nil } retTyps = append(retTyps, retTyp) } + + curFile, _ := moreiters.First(curRet.Ancestors((*ast.File)(nil))) + file := curFile.Node().(*ast.File) matches := analysisinternal.MatchingIdents(retTyps, file, ret.Pos(), info, pass.Pkg) qual := typesinternal.FileQualifier(file, pass.Pkg) for i, retTyp := range retTyps { @@ -215,8 +177,8 @@ outer: } pass.Report(analysis.Diagnostic{ - Pos: typeErr.Pos, - End: typeErrEndPos, + Pos: start, + End: end, Message: typeErr.Msg, SuggestedFixes: []analysis.SuggestedFix{{ Message: "Fill in return values", @@ -255,7 +217,7 @@ var wrongReturnNumRegexes = []*regexp.Regexp{ regexp.MustCompile(`not enough return values`), } -func FixesError(err types.Error) bool { +func fixesError(err types.Error) bool { msg := strings.TrimSpace(err.Msg) for _, rx := range wrongReturnNumRegexes { if rx.MatchString(msg) { @@ -264,3 +226,12 @@ func FixesError(err types.Error) bool { } return false } + +// enclosingFunc returns the cursor for the innermost Func{Decl,Lit} +// that encloses c, if any. +func enclosingFunc(c cursor.Cursor) (cursor.Cursor, bool) { + for curAncestor := range c.Ancestors((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) { + return curAncestor, true + } + return cursor.Cursor{}, false +} diff --git a/gopls/internal/analysis/nonewvars/nonewvars.go b/gopls/internal/analysis/nonewvars/nonewvars.go index 9e5d79df02c..8a3bf502c51 100644 --- a/gopls/internal/analysis/nonewvars/nonewvars.go +++ b/gopls/internal/analysis/nonewvars/nonewvars.go @@ -7,16 +7,17 @@ package nonewvars import ( - "bytes" _ "embed" "go/ast" - "go/format" "go/token" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/util/moreiters" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/astutil/cursor" + "golang.org/x/tools/internal/typesinternal" ) //go:embed doc.go @@ -33,57 +34,45 @@ var Analyzer = &analysis.Analyzer{ func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - if len(pass.TypeErrors) == 0 { - return nil, nil - } - nodeFilter := []ast.Node{(*ast.AssignStmt)(nil)} - inspect.Preorder(nodeFilter, func(n ast.Node) { - assignStmt, _ := n.(*ast.AssignStmt) - // We only care about ":=". - if assignStmt.Tok != token.DEFINE { - return + for _, typeErr := range pass.TypeErrors { + if typeErr.Msg != "no new variables on left side of :=" { + continue // irrelevant error + } + _, start, end, ok := typesinternal.ErrorCodeStartEnd(typeErr) + if !ok { + continue // can't get position info + } + curErr, ok := cursor.Root(inspect).FindPos(start, end) + if !ok { + continue // can't find errant node } - var file *ast.File - for _, f := range pass.Files { - if f.FileStart <= assignStmt.Pos() && assignStmt.Pos() < f.FileEnd { - file = f - break + // Find enclosing assignment (which may be curErr itself). + assign, ok := curErr.Node().(*ast.AssignStmt) + if !ok { + cur, ok := moreiters.First(curErr.Ancestors((*ast.AssignStmt)(nil))) + if !ok { + continue // no enclosing assignment } + assign = cur.Node().(*ast.AssignStmt) } - if file == nil { - return + if assign.Tok != token.DEFINE { + continue // not a := statement } - for _, err := range pass.TypeErrors { - if !FixesError(err.Msg) { - continue - } - if assignStmt.Pos() > err.Pos || err.Pos >= assignStmt.End() { - continue - } - var buf bytes.Buffer - if err := format.Node(&buf, pass.Fset, file); err != nil { - continue - } - pass.Report(analysis.Diagnostic{ - Pos: err.Pos, - End: analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos), - Message: err.Msg, - SuggestedFixes: []analysis.SuggestedFix{{ - Message: "Change ':=' to '='", - TextEdits: []analysis.TextEdit{{ - Pos: err.Pos, - End: err.Pos + 1, - }}, + pass.Report(analysis.Diagnostic{ + Pos: assign.TokPos, + End: assign.TokPos + token.Pos(len(":=")), + Message: typeErr.Msg, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Change ':=' to '='", + TextEdits: []analysis.TextEdit{{ + Pos: assign.TokPos, + End: assign.TokPos + token.Pos(len(":")), }}, - }) - } - }) + }}, + }) + } return nil, nil } - -func FixesError(msg string) bool { - return msg == "no new variables on left side of :=" -} diff --git a/gopls/internal/analysis/noresultvalues/noresultvalues.go b/gopls/internal/analysis/noresultvalues/noresultvalues.go index 118beb4568b..fe979f52aac 100644 --- a/gopls/internal/analysis/noresultvalues/noresultvalues.go +++ b/gopls/internal/analysis/noresultvalues/noresultvalues.go @@ -5,9 +5,8 @@ package noresultvalues import ( - "bytes" "go/ast" - "go/format" + "go/token" "strings" _ "embed" @@ -15,7 +14,10 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/util/moreiters" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/astutil/cursor" + "golang.org/x/tools/internal/typesinternal" ) //go:embed doc.go @@ -32,55 +34,40 @@ var Analyzer = &analysis.Analyzer{ func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - if len(pass.TypeErrors) == 0 { - return nil, nil - } - - nodeFilter := []ast.Node{(*ast.ReturnStmt)(nil)} - inspect.Preorder(nodeFilter, func(n ast.Node) { - retStmt, _ := n.(*ast.ReturnStmt) - var file *ast.File - for _, f := range pass.Files { - if f.FileStart <= retStmt.Pos() && retStmt.Pos() < f.FileEnd { - file = f - break - } + for _, typErr := range pass.TypeErrors { + if !fixesError(typErr.Msg) { + continue // irrelevant error } - if file == nil { - return + _, start, end, ok := typesinternal.ErrorCodeStartEnd(typErr) + if !ok { + continue // can't get position info } - - for _, err := range pass.TypeErrors { - if !FixesError(err.Msg) { - continue - } - if retStmt.Pos() >= err.Pos || err.Pos >= retStmt.End() { - continue - } - var buf bytes.Buffer - if err := format.Node(&buf, pass.Fset, file); err != nil { - continue - } + curErr, ok := cursor.Root(inspect).FindPos(start, end) + if !ok { + continue // can't find errant node + } + // Find first enclosing return statement, if any. + if curRet, ok := moreiters.First(curErr.Ancestors((*ast.ReturnStmt)(nil))); ok { + ret := curRet.Node() pass.Report(analysis.Diagnostic{ - Pos: err.Pos, - End: analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos), - Message: err.Msg, + Pos: start, + End: end, + Message: typErr.Msg, SuggestedFixes: []analysis.SuggestedFix{{ Message: "Delete return values", TextEdits: []analysis.TextEdit{{ - Pos: retStmt.Pos(), - End: retStmt.End(), - NewText: []byte("return"), + Pos: ret.Pos() + token.Pos(len("return")), + End: ret.End(), }}, }}, }) } - }) + } return nil, nil } -func FixesError(msg string) bool { +func fixesError(msg string) bool { return msg == "no result values expected" || strings.HasPrefix(msg, "too many return values") && strings.Contains(msg, "want ()") } diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index d094c535d7a..a3aff5e5475 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -2001,7 +2001,7 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs *typeCheckInputs, errs [ batch := func(related []types.Error) { var diags []*Diagnostic for i, e := range related { - code, start, end, ok := typesinternal.ReadGo116ErrorData(e) + code, start, end, ok := typesinternal.ErrorCodeStartEnd(e) if !ok || !start.IsValid() || !end.IsValid() { start, end = e.Pos, e.Pos code = 0 @@ -2075,6 +2075,9 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs *typeCheckInputs, errs [ if end == start { // Expand the end position to a more meaningful span. + // + // TODO(adonovan): It is the type checker's responsibility + // to ensure that (start, end) are meaningful; see #71803. end = analysisinternal.TypeErrorEndPos(e.Fset, pgf.Src, start) // debugging golang/go#65960 diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 34ac7426019..f82c32d6a9c 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -309,7 +309,7 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error { for _, typeError := range req.pkg.TypeErrors() { // Does type error overlap with CodeAction range? start, end := typeError.Pos, typeError.Pos - if _, _, endPos, ok := typesinternal.ReadGo116ErrorData(typeError); ok { + if _, _, endPos, ok := typesinternal.ErrorCodeStartEnd(typeError); ok { end = endPos } typeErrorRange, err := req.pgf.PosRange(start, end) diff --git a/gopls/internal/test/marker/testdata/highlight/controlflow.txt b/gopls/internal/test/marker/testdata/highlight/controlflow.txt index c09f748a553..46ec48d030d 100644 --- a/gopls/internal/test/marker/testdata/highlight/controlflow.txt +++ b/gopls/internal/test/marker/testdata/highlight/controlflow.txt @@ -68,7 +68,6 @@ func _() { } func _() () { - // TODO(golang/go#65966): fix the triplicate diagnostics here. - return 0 //@hiloc(ret2, "0", text), diag("0", re"too many return"), diag("0", re"too many return"), diag("0", re"too many return") + return 0 //@hiloc(ret2, "0", text), diag("0", re"too many return") //@highlight(ret2, ret2) } diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index d96d22982c5..aba435fa404 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -23,6 +23,8 @@ import ( "golang.org/x/tools/internal/typesinternal" ) +// Deprecated: this heuristic is ill-defined. +// TODO(adonovan): move to sole use in gopls/internal/cache. func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos { // Get the end position for the type error. file := fset.File(start) diff --git a/internal/typesinternal/types.go b/internal/typesinternal/types.go index 34534879630..edf0347ec3b 100644 --- a/internal/typesinternal/types.go +++ b/internal/typesinternal/types.go @@ -32,12 +32,14 @@ func SetUsesCgo(conf *types.Config) bool { return true } -// ReadGo116ErrorData extracts additional information from types.Error values +// ErrorCodeStartEnd extracts additional information from types.Error values // generated by Go version 1.16 and later: the error code, start position, and // end position. If all positions are valid, start <= err.Pos <= end. // // If the data could not be read, the final result parameter will be false. -func ReadGo116ErrorData(err types.Error) (code ErrorCode, start, end token.Pos, ok bool) { +// +// TODO(adonovan): eliminate start/end when proposal #71803 is accepted. +func ErrorCodeStartEnd(err types.Error) (code ErrorCode, start, end token.Pos, ok bool) { var data [3]int // By coincidence all of these fields are ints, which simplifies things. v := reflect.ValueOf(err) From fe883a85d1827c1acaed7273c9a10ff0660d9a0d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 18 Feb 2025 13:06:42 -0500 Subject: [PATCH 30/99] gopls/internal/analysis/unusedvariable: refine bug.Report golang/go#71812 This CL adds assertions to refine the bug reported in golang/go#71812, in which the analyzer reports an invalid SuggestedFix. Updates golang/go#71812 Change-Id: Ie4a9aac9ba3d16974320d7cd4b48bc4cc76fc3c4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650395 Commit-Queue: Alan Donovan Reviewed-by: Jonathan Amsterdam Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- .../analysis/unusedvariable/unusedvariable.go | 81 ++++++++++--------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/gopls/internal/analysis/unusedvariable/unusedvariable.go b/gopls/internal/analysis/unusedvariable/unusedvariable.go index 15bcd43d873..5f1c188eb6a 100644 --- a/gopls/internal/analysis/unusedvariable/unusedvariable.go +++ b/gopls/internal/analysis/unusedvariable/unusedvariable.go @@ -13,10 +13,12 @@ import ( "go/token" "go/types" "regexp" + "slices" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" ) @@ -165,16 +167,13 @@ func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.Valu // Find parent DeclStmt and delete it for _, node := range path { if declStmt, ok := node.(*ast.DeclStmt); ok { - edits := deleteStmtFromBlock(pass.Fset, path, declStmt) - if len(edits) == 0 { - return nil // can this happen? - } - return []analysis.SuggestedFix{ - { + if edits := deleteStmtFromBlock(pass.Fset, path, declStmt); len(edits) > 0 { + return []analysis.SuggestedFix{{ Message: suggestedFixMessage(ident.Name), TextEdits: edits, - }, + }} } + return nil } } } @@ -222,16 +221,13 @@ func removeVariableFromAssignment(fset *token.FileSet, path []ast.Node, stmt *as } // RHS does not have any side effects, delete the whole statement - edits := deleteStmtFromBlock(fset, path, stmt) - if len(edits) == 0 { - return nil // can this happen? - } - return []analysis.SuggestedFix{ - { + if edits := deleteStmtFromBlock(fset, path, stmt); len(edits) > 0 { + return []analysis.SuggestedFix{{ Message: suggestedFixMessage(ident.Name), TextEdits: edits, - }, + }} } + return nil } // Otherwise replace ident with `_` @@ -253,34 +249,48 @@ func suggestedFixMessage(name string) string { return fmt.Sprintf("Remove variable %s", name) } +// deleteStmtFromBlock returns the edits to remove stmt if its parent is a BlockStmt. +// (stmt is not necessarily the leaf, path[0].) +// +// It returns nil if the parent is not a block, as in these examples: +// +// switch STMT; {} +// switch { default: STMT } +// select { default: STMT } +// +// TODO(adonovan): handle these cases too. func deleteStmtFromBlock(fset *token.FileSet, path []ast.Node, stmt ast.Stmt) []analysis.TextEdit { - // Find innermost enclosing BlockStmt. - var block *ast.BlockStmt - for i := range path { - if blockStmt, ok := path[i].(*ast.BlockStmt); ok { - block = blockStmt - break - } + // TODO(adonovan): simplify using Cursor API. + i := slices.Index(path, ast.Node(stmt)) // must be present + block, ok := path[i+1].(*ast.BlockStmt) + if !ok { + return nil // parent is not a BlockStmt } - nodeIndex := -1 - for i, blockStmt := range block.List { - if blockStmt == stmt { - nodeIndex = i - break - } + nodeIndex := slices.Index(block.List, stmt) + if nodeIndex == -1 { + bug.Reportf("%s: Stmt not found in BlockStmt.List", safetoken.StartPosition(fset, stmt.Pos())) // refine #71812 + return nil } - // The statement we need to delete was not found in BlockStmt - if nodeIndex == -1 { + if !stmt.Pos().IsValid() { + bug.Reportf("%s: invalid Stmt.Pos", safetoken.StartPosition(fset, stmt.Pos())) // refine #71812 return nil } // Delete until the end of the block unless there is another statement after // the one we are trying to delete end := block.Rbrace + if !end.IsValid() { + bug.Reportf("%s: BlockStmt has no Rbrace", safetoken.StartPosition(fset, block.Pos())) // refine #71812 + return nil + } if nodeIndex < len(block.List)-1 { end = block.List[nodeIndex+1].Pos() + if end < stmt.Pos() { + bug.Reportf("%s: BlockStmt.List[last].Pos > BlockStmt.Rbrace", safetoken.StartPosition(fset, block.Pos())) // refine #71812 + return nil + } } // Account for comments within the block containing the statement @@ -298,7 +308,7 @@ outer: // If a comment exists within the current block, after the unused variable statement, // and before the next statement, we shouldn't delete it. if coLine > stmtEndLine { - end = co.Pos() + end = co.Pos() // preserves invariant stmt.Pos <= end (#71812) break outer } if co.Pos() > end { @@ -308,12 +318,11 @@ outer: } } - return []analysis.TextEdit{ - { - Pos: stmt.Pos(), - End: end, - }, - } + // Delete statement and optional following comment. + return []analysis.TextEdit{{ + Pos: stmt.Pos(), + End: end, + }} } // exprMayHaveSideEffects reports whether the expression may have side effects From 4b3fdfd83f78b8336aef4160183bf5b27febc5b9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sun, 16 Feb 2025 22:19:08 -0500 Subject: [PATCH 31/99] go/analysis/passes/printf: suppress diagnostic for Println("...%XX...") A common form of literal string is a URL containing URL-escaped characters. This CL causes the printf checker not to report a "Println call has possible Printf formatting directive" diagnostic for it. + test Fixes golang/go#29854 Change-Id: Ib1dcc44dd8185da17f61296632ad030cb1e58420 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650175 LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Commit-Queue: Alan Donovan Reviewed-by: Jonathan Amsterdam --- go/analysis/passes/printf/printf.go | 18 +++++++++++++++--- go/analysis/passes/printf/testdata/src/a/a.go | 2 ++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index 81600a283aa..a28ed365d1e 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -924,9 +924,14 @@ func checkPrint(pass *analysis.Pass, call *ast.CallExpr, name string) { // The % in "abc 0.0%" couldn't be a formatting directive. s = strings.TrimSuffix(s, "%") if strings.Contains(s, "%") { - m := printFormatRE.FindStringSubmatch(s) - if m != nil { - pass.ReportRangef(call, "%s call has possible Printf formatting directive %s", name, m[0]) + for _, m := range printFormatRE.FindAllString(s, -1) { + // Allow %XX where XX are hex digits, + // as this is common in URLs. + if len(m) >= 3 && isHex(m[1]) && isHex(m[2]) { + continue + } + pass.ReportRangef(call, "%s call has possible Printf formatting directive %s", name, m) + break // report only the first one } } } @@ -992,3 +997,10 @@ func (ss stringSet) Set(flag string) error { // // Remove this after the 1.24 release. var suppressNonconstants bool + +// isHex reports whether b is a hex digit. +func isHex(b byte) bool { + return '0' <= b && b <= '9' || + 'A' <= b && b <= 'F' || + 'a' <= b && b <= 'f' +} diff --git a/go/analysis/passes/printf/testdata/src/a/a.go b/go/analysis/passes/printf/testdata/src/a/a.go index 02ce425f8a3..da48f98f0a8 100644 --- a/go/analysis/passes/printf/testdata/src/a/a.go +++ b/go/analysis/passes/printf/testdata/src/a/a.go @@ -154,6 +154,8 @@ func PrintfTests() { fmt.Println("%v", "hi") // want "fmt.Println call has possible Printf formatting directive %v" fmt.Println("%T", "hi") // want "fmt.Println call has possible Printf formatting directive %T" fmt.Println("%s"+" there", "hi") // want "fmt.Println call has possible Printf formatting directive %s" + fmt.Println("http://foo.com?q%2Fabc") // no diagnostic: %XX is excepted + fmt.Println("http://foo.com?q%2Fabc-%s") // want"fmt.Println call has possible Printf formatting directive %s" fmt.Println("0.0%") // correct (trailing % couldn't be a formatting directive) fmt.Printf("%s", "hi", 3) // want "fmt.Printf call needs 1 arg but has 2 args" _ = fmt.Sprintf("%"+("s"), "hi", 3) // want "fmt.Sprintf call needs 1 arg but has 2 args" From ad5dd9875168fe5cb7c43643a34c8cc26411b2f9 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Tue, 18 Feb 2025 20:49:25 +0000 Subject: [PATCH 32/99] gopls: fix a few bugs related to the new modcache imports source Fix the following bugs related to the new "gopls" imports source and module cache index: - Only construct the modcacheState if the imports source is "gopls", which is not yet the default. This was causing memory regressions, as the modcache table is non-trivial. - Add missing error handling. - Don't call modcacheState.stopTimer if the modcacheState is nil, which may already have been the case with "importsSource": "off". For golang/go#71607 Change-Id: I33c90ee4b97c8675b342cb0c045eef183a1ef365 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650397 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/session.go | 7 ++++++- gopls/internal/cache/view.go | 9 +++++++-- gopls/internal/golang/format.go | 6 ++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go index a7fb618f679..5ae753eb91c 100644 --- a/gopls/internal/cache/session.go +++ b/gopls/internal/cache/session.go @@ -238,7 +238,12 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, * viewDefinition: def, importsState: newImportsState(backgroundCtx, s.cache.modCache, pe), } - if def.folder.Options.ImportsSource != settings.ImportsSourceOff { + + // Keep this in sync with golang.computeImportEdits. + // + // TODO(rfindley): encapsulate the imports state logic so that the handling + // for Options.ImportsSource is in a single location. + if def.folder.Options.ImportsSource == settings.ImportsSourceGopls { v.modcacheState = newModcacheState(def.folder.Env.GOMODCACHE) } diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go index 26f0de86125..6ebf6837ef2 100644 --- a/gopls/internal/cache/view.go +++ b/gopls/internal/cache/view.go @@ -109,7 +109,10 @@ type View struct { // importsState is for the old imports code importsState *importsState - // maintain the current module cache index + // modcacheState is the replacement for importsState, to be used for + // goimports operations when the imports source is "gopls". + // + // It may be nil, if the imports source is not "gopls". modcacheState *modcacheState // pkgIndex is an index of package IDs, for efficient storage of typerefs. @@ -492,7 +495,9 @@ func (v *View) shutdown() { // Cancel the initial workspace load if it is still running. v.cancelInitialWorkspaceLoad() v.importsState.stopTimer() - v.modcacheState.stopTimer() + if v.modcacheState != nil { + v.modcacheState.stopTimer() + } v.snapshotMu.Lock() if v.snapshot != nil { diff --git a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go index de4ec3a642c..acc619eba0c 100644 --- a/gopls/internal/golang/format.go +++ b/gopls/internal/golang/format.go @@ -137,7 +137,13 @@ func computeImportEdits(ctx context.Context, pgf *parsego.File, snapshot *cache. // Build up basic information about the original file. isource, err := imports.NewProcessEnvSource(options.Env, filename, pgf.File.Name.Name) + if err != nil { + return nil, nil, err + } var source imports.Source + + // Keep this in sync with [cache.Session.createView] (see the TODO there: we + // should factor out the handling of the ImportsSource setting). switch snapshot.Options().ImportsSource { case settings.ImportsSourceGopls: source = snapshot.NewGoplsSource(isource) From df7baa073c7b4850753e4f6b6084402fd9cb573b Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 13 Feb 2025 13:55:48 -0500 Subject: [PATCH 33/99] gopls/internal/analysis/simplifyrange: more precise fix This CL reduces the size of the fix offered by simplifyrange, which makes the cursor jump less. It's also simpler, and handles a missing case. Change-Id: I8dbff96158e442b2073e86694b61ea1f0b1ea704 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649355 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- .../analysis/simplifyrange/simplifyrange.go | 93 +++++++------------ .../simplifyrange/simplifyrange_test.go | 11 +-- .../simplifyrange/testdata/src/a/a.go | 7 ++ .../simplifyrange/testdata/src/a/a.go.golden | 7 ++ 4 files changed, 51 insertions(+), 67 deletions(-) diff --git a/gopls/internal/analysis/simplifyrange/simplifyrange.go b/gopls/internal/analysis/simplifyrange/simplifyrange.go index 6d079059eb1..fd685ba2c5b 100644 --- a/gopls/internal/analysis/simplifyrange/simplifyrange.go +++ b/gopls/internal/analysis/simplifyrange/simplifyrange.go @@ -5,10 +5,8 @@ package simplifyrange import ( - "bytes" _ "embed" "go/ast" - "go/printer" "go/token" "golang.org/x/tools/go/analysis" @@ -42,73 +40,48 @@ func run(pass *analysis.Pass) (interface{}, error) { (*ast.RangeStmt)(nil), } inspect.Preorder(nodeFilter, func(n ast.Node) { - if _, ok := generated[pass.Fset.File(n.Pos())]; ok { - return // skip checking if it's generated code - } + rng := n.(*ast.RangeStmt) - var copy *ast.RangeStmt // shallow-copy the AST before modifying - { - x := *n.(*ast.RangeStmt) - copy = &x - } - end := newlineIndex(pass.Fset, copy) + kblank := isBlank(rng.Key) + vblank := isBlank(rng.Value) + var start, end token.Pos + switch { + case kblank && (rng.Value == nil || vblank): + // for _ = range x {} + // for _, _ = range x {} + // ^^^^^^^ + start, end = rng.Key.Pos(), rng.Range - // Range statements of the form: for i, _ := range x {} - var old ast.Expr - if isBlank(copy.Value) { - old = copy.Value - copy.Value = nil - } - // Range statements of the form: for _ := range x {} - if isBlank(copy.Key) && copy.Value == nil { - old = copy.Key - copy.Key = nil + case vblank: + // for k, _ := range x {} + // ^^^ + start, end = rng.Key.End(), rng.Value.End() + + default: + return } - // Return early if neither if condition is met. - if old == nil { + + if generated[pass.Fset.File(n.Pos())] { return } + pass.Report(analysis.Diagnostic{ - Pos: old.Pos(), - End: old.End(), - Message: "simplify range expression", - SuggestedFixes: suggestedFixes(pass.Fset, copy, end), + Pos: start, + End: end, + Message: "simplify range expression", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Remove empty value", + TextEdits: []analysis.TextEdit{{ + Pos: start, + End: end, + }}, + }}, }) }) return nil, nil } -func suggestedFixes(fset *token.FileSet, rng *ast.RangeStmt, end token.Pos) []analysis.SuggestedFix { - var b bytes.Buffer - printer.Fprint(&b, fset, rng) - stmt := b.Bytes() - index := bytes.Index(stmt, []byte("\n")) - // If there is a new line character, then don't replace the body. - if index != -1 { - stmt = stmt[:index] - } - return []analysis.SuggestedFix{{ - Message: "Remove empty value", - TextEdits: []analysis.TextEdit{{ - Pos: rng.Pos(), - End: end, - NewText: stmt[:index], - }}, - }} -} - -func newlineIndex(fset *token.FileSet, rng *ast.RangeStmt) token.Pos { - var b bytes.Buffer - printer.Fprint(&b, fset, rng) - contents := b.Bytes() - index := bytes.Index(contents, []byte("\n")) - if index == -1 { - return rng.End() - } - return rng.Pos() + token.Pos(index) -} - -func isBlank(x ast.Expr) bool { - ident, ok := x.(*ast.Ident) - return ok && ident.Name == "_" +func isBlank(e ast.Expr) bool { + id, ok := e.(*ast.Ident) + return ok && id.Name == "_" } diff --git a/gopls/internal/analysis/simplifyrange/simplifyrange_test.go b/gopls/internal/analysis/simplifyrange/simplifyrange_test.go index 50a600e03bf..089f65df870 100644 --- a/gopls/internal/analysis/simplifyrange/simplifyrange_test.go +++ b/gopls/internal/analysis/simplifyrange/simplifyrange_test.go @@ -5,8 +5,6 @@ package simplifyrange_test import ( - "go/build" - "slices" "testing" "golang.org/x/tools/go/analysis/analysistest" @@ -14,9 +12,8 @@ import ( ) func Test(t *testing.T) { - testdata := analysistest.TestData() - analysistest.RunWithSuggestedFixes(t, testdata, simplifyrange.Analyzer, "a", "generatedcode") - if slices.Contains(build.Default.ReleaseTags, "go1.23") { // uses iter.Seq - analysistest.RunWithSuggestedFixes(t, testdata, simplifyrange.Analyzer, "rangeoverfunc") - } + analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), simplifyrange.Analyzer, + "a", + "generatedcode", + "rangeoverfunc") } diff --git a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go index 49face1e968..1d7b1bd58f2 100644 --- a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go +++ b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go @@ -13,4 +13,11 @@ func m() { } for _ = range maps { // want "simplify range expression" } + for _, _ = range maps { // want "simplify range expression" + } + for _, v := range maps { // nope + println(v) + } + for range maps { // nope + } } diff --git a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden index ec8490ab337..25139bd93f2 100644 --- a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden +++ b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden @@ -13,4 +13,11 @@ func m() { } for range maps { // want "simplify range expression" } + for range maps { // want "simplify range expression" + } + for _, v := range maps { // nope + println(v) + } + for range maps { // nope + } } From 776604a9ed881ee1274724fb3a5f058f3ebdf0eb Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 17 Feb 2025 12:24:06 -0500 Subject: [PATCH 34/99] gopls/internal/analysis/modernize: sortslice: fix crash The sole statement of a comparison func body is not necessarily a return statement. + Test Fixes golang/go#71786 Change-Id: Ic002035fc9fa303b62ed1828c13f3bdfb8bc6950 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650215 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- .../internal/analysis/modernize/sortslice.go | 75 ++++++++++--------- .../testdata/src/sortslice/sortslice.go | 12 ++- .../src/sortslice/sortslice.go.golden | 12 ++- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/gopls/internal/analysis/modernize/sortslice.go b/gopls/internal/analysis/modernize/sortslice.go index 7f695d76495..bbc8befb8ee 100644 --- a/gopls/internal/analysis/modernize/sortslice.go +++ b/gopls/internal/analysis/modernize/sortslice.go @@ -57,45 +57,46 @@ func sortslice(pass *analysis.Pass) { i := sig.Params().At(0) j := sig.Params().At(1) - ret := lit.Body.List[0].(*ast.ReturnStmt) - if compare, ok := ret.Results[0].(*ast.BinaryExpr); ok && compare.Op == token.LSS { - // isIndex reports whether e is s[v]. - isIndex := func(e ast.Expr, v *types.Var) bool { - index, ok := e.(*ast.IndexExpr) - return ok && - equalSyntax(index.X, s) && - is[*ast.Ident](index.Index) && - info.Uses[index.Index.(*ast.Ident)] == v - } - if isIndex(compare.X, i) && isIndex(compare.Y, j) { - // Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) + if ret, ok := lit.Body.List[0].(*ast.ReturnStmt); ok { + if compare, ok := ret.Results[0].(*ast.BinaryExpr); ok && compare.Op == token.LSS { + // isIndex reports whether e is s[v]. + isIndex := func(e ast.Expr, v *types.Var) bool { + index, ok := e.(*ast.IndexExpr) + return ok && + equalSyntax(index.X, s) && + is[*ast.Ident](index.Index) && + info.Uses[index.Index.(*ast.Ident)] == v + } + if isIndex(compare.X, i) && isIndex(compare.Y, j) { + // Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) - _, prefix, importEdits := analysisinternal.AddImport( - info, file, "slices", "slices", "Sort", call.Pos()) + _, prefix, importEdits := analysisinternal.AddImport( + info, file, "slices", "slices", "Sort", call.Pos()) - pass.Report(analysis.Diagnostic{ - // Highlight "sort.Slice". - Pos: call.Fun.Pos(), - End: call.Fun.End(), - Category: "sortslice", - Message: fmt.Sprintf("sort.Slice can be modernized using slices.Sort"), - SuggestedFixes: []analysis.SuggestedFix{{ - Message: fmt.Sprintf("Replace sort.Slice call by slices.Sort"), - TextEdits: append(importEdits, []analysis.TextEdit{ - { - // Replace sort.Slice with slices.Sort. - Pos: call.Fun.Pos(), - End: call.Fun.End(), - NewText: []byte(prefix + "Sort"), - }, - { - // Eliminate FuncLit. - Pos: call.Args[0].End(), - End: call.Rparen, - }, - }...), - }}, - }) + pass.Report(analysis.Diagnostic{ + // Highlight "sort.Slice". + Pos: call.Fun.Pos(), + End: call.Fun.End(), + Category: "sortslice", + Message: fmt.Sprintf("sort.Slice can be modernized using slices.Sort"), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace sort.Slice call by slices.Sort"), + TextEdits: append(importEdits, []analysis.TextEdit{ + { + // Replace sort.Slice with slices.Sort. + Pos: call.Fun.Pos(), + End: call.Fun.End(), + NewText: []byte(prefix + "Sort"), + }, + { + // Eliminate FuncLit. + Pos: call.Args[0].End(), + End: call.Rparen, + }, + }...), + }}, + }) + } } } } diff --git a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go index 53d15746839..19242065b24 100644 --- a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go +++ b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go @@ -20,6 +20,16 @@ func _(s []int) { sort.Slice(s, func(i, j int) bool { return s[j] < s[i] }) // nope: wrong index var } -func _(s2 []struct{ x int }) { +func _(sense bool, s2 []struct{ x int }) { sort.Slice(s2, func(i, j int) bool { return s2[i].x < s2[j].x }) // nope: not a simple index operation + + // Regression test for a crash: the sole statement of a + // comparison func body is not necessarily a return! + sort.Slice(s2, func(i, j int) bool { + if sense { + return s2[i].x < s2[j].x + } else { + return s2[i].x > s2[j].x + } + }) } diff --git a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden index 34af5aad60b..19149b4480a 100644 --- a/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/sortslice/sortslice.go.golden @@ -22,6 +22,16 @@ func _(s []int) { sort.Slice(s, func(i, j int) bool { return s[j] < s[i] }) // nope: wrong index var } -func _(s2 []struct{ x int }) { +func _(sense bool, s2 []struct{ x int }) { sort.Slice(s2, func(i, j int) bool { return s2[i].x < s2[j].x }) // nope: not a simple index operation + + // Regression test for a crash: the sole statement of a + // comparison func body is not necessarily a return! + sort.Slice(s2, func(i, j int) bool { + if sense { + return s2[i].x < s2[j].x + } else { + return s2[i].x > s2[j].x + } + }) } From e6754cedb2986a3026a6de6e1460f6a2edbe01f0 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 18 Feb 2025 14:43:35 -0500 Subject: [PATCH 35/99] gopls/internal/cache/parsego: add File.Cursor, and use it This CL adds a Cursor to the parsego.File. Though the primary motivation is convenience and flexibility, it is expected to be an optimization: though it is computed eagerly, it is retained in the parse cache, and is expected to pay for itself very quickly by allowing us to replace many whole-File ast.Inspect operations with more targeted traversals. The CL replaces all ast.Inspect(file) operations with Cursor, but there remain many more opportunities for using it in narrower traversals, and in places that need to navigate to siblings or ancestors. Also, amend Cursor.FindPos to use the complete range of the File, as CL 637738 recently did for astutil.NodeContains. Also, various clean-ups to InlayHint: - push the traversals down in InlayHint to avoid having to scan a slice for every single node we visit; - simplify the function signature used for each hint algorithm. Change-Id: I64d0c2cae75fd73a4b539ceb81ad9d6f7d80cfb8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650396 Reviewed-by: Jonathan Amsterdam Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Commit-Queue: Alan Donovan Reviewed-by: Robert Findley --- gopls/internal/cache/parsego/file.go | 8 + gopls/internal/cache/parsego/parse.go | 8 + gopls/internal/cache/xrefs/xrefs.go | 11 +- gopls/internal/golang/folding_range.go | 142 ++++---- gopls/internal/golang/implementation.go | 20 +- gopls/internal/golang/inlay_hint.go | 414 ++++++++++++------------ gopls/internal/golang/references.go | 12 +- gopls/internal/server/link.go | 16 +- internal/astutil/cursor/cursor.go | 12 +- 9 files changed, 326 insertions(+), 317 deletions(-) diff --git a/gopls/internal/cache/parsego/file.go b/gopls/internal/cache/parsego/file.go index 41fd1937ec1..2be4ed4b2ca 100644 --- a/gopls/internal/cache/parsego/file.go +++ b/gopls/internal/cache/parsego/file.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/astutil/cursor" ) // A File contains the results of parsing a Go file. @@ -32,6 +33,8 @@ type File struct { // actual content of the file if we have fixed the AST. Src []byte + Cursor cursor.Cursor // cursor of *ast.File, sans sibling files + // fixedSrc and fixedAST report on "fixing" that occurred during parsing of // this file. // @@ -71,6 +74,11 @@ func (pgf *File) PositionPos(p protocol.Position) (token.Pos, error) { return safetoken.Pos(pgf.Tok, offset) } +// PosPosition returns a protocol Position for the token.Pos in this file. +func (pgf *File) PosPosition(pos token.Pos) (protocol.Position, error) { + return pgf.Mapper.PosPosition(pgf.Tok, pos) +} + // PosRange returns a protocol Range for the token.Pos interval in this file. func (pgf *File) PosRange(start, end token.Pos) (protocol.Range, error) { return pgf.Mapper.PosRange(pgf.Tok, start, end) diff --git a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go index df167314b04..db6089d8e6d 100644 --- a/gopls/internal/cache/parsego/parse.go +++ b/gopls/internal/cache/parsego/parse.go @@ -23,11 +23,13 @@ import ( "reflect" "slices" + "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" ) @@ -153,6 +155,11 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s } assert(file != nil, "nil *ast.File") + // Provide a cursor for fast and convenient navigation. + inspect := inspector.New([]*ast.File{file}) + curFile, _ := cursor.Root(inspect).FirstChild() + _ = curFile.Node().(*ast.File) + return &File{ URI: uri, Mode: mode, @@ -161,6 +168,7 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s fixedAST: fixedAST, File: file, Tok: tok, + Cursor: curFile, Mapper: protocol.NewMapper(uri, src), ParseErr: parseErr, }, fixes diff --git a/gopls/internal/cache/xrefs/xrefs.go b/gopls/internal/cache/xrefs/xrefs.go index 2115322bfdc..d9b7051737a 100644 --- a/gopls/internal/cache/xrefs/xrefs.go +++ b/gopls/internal/cache/xrefs/xrefs.go @@ -44,8 +44,8 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte { objectpathFor := new(objectpath.Encoder).For for fileIndex, pgf := range files { - ast.Inspect(pgf.File, func(n ast.Node) bool { - switch n := n.(type) { + for cur := range pgf.Cursor.Preorder((*ast.Ident)(nil), (*ast.ImportSpec)(nil)) { + switch n := cur.Node().(type) { case *ast.Ident: // Report a reference for each identifier that // uses a symbol exported from another package. @@ -68,7 +68,7 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte { if err != nil { // Capitalized but not exported // (e.g. local const/var/type). - return true + continue } gobObj = &gobObject{Path: path} objects[obj] = gobObj @@ -91,7 +91,7 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte { // string to the imported package. pkgname := info.PkgNameOf(n) if pkgname == nil { - return true // missing import + continue // missing import } objects := getObjects(pkgname.Imported()) gobObj, ok := objects[nil] @@ -109,8 +109,7 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte { bug.Reportf("out of bounds import spec %+v", n.Path) } } - return true - }) + } } // Flatten the maps into slices, and sort for determinism. diff --git a/gopls/internal/golang/folding_range.go b/gopls/internal/golang/folding_range.go index 4352da28151..eed31e92944 100644 --- a/gopls/internal/golang/folding_range.go +++ b/gopls/internal/golang/folding_range.go @@ -46,12 +46,84 @@ func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, ranges := commentsFoldingRange(pgf) // Walk the ast and collect folding ranges. - ast.Inspect(pgf.File, func(n ast.Node) bool { - if rng, ok := foldingRangeFunc(pgf, n, lineFoldingOnly); ok { - ranges = append(ranges, rng) + filter := []ast.Node{ + (*ast.BasicLit)(nil), + (*ast.BlockStmt)(nil), + (*ast.CallExpr)(nil), + (*ast.CaseClause)(nil), + (*ast.CommClause)(nil), + (*ast.CompositeLit)(nil), + (*ast.FieldList)(nil), + (*ast.GenDecl)(nil), + } + for cur := range pgf.Cursor.Preorder(filter...) { + // TODO(suzmue): include trailing empty lines before the closing + // parenthesis/brace. + var kind protocol.FoldingRangeKind + // start and end define the range of content to fold away. + var start, end token.Pos + switch n := cur.Node().(type) { + case *ast.BlockStmt: + // Fold between positions of or lines between "{" and "}". + start, end = getLineFoldingRange(pgf, n.Lbrace, n.Rbrace, lineFoldingOnly) + + case *ast.CaseClause: + // Fold from position of ":" to end. + start, end = n.Colon+1, n.End() + + case *ast.CommClause: + // Fold from position of ":" to end. + start, end = n.Colon+1, n.End() + + case *ast.CallExpr: + // Fold between positions of or lines between "(" and ")". + start, end = getLineFoldingRange(pgf, n.Lparen, n.Rparen, lineFoldingOnly) + + case *ast.FieldList: + // Fold between positions of or lines between opening parenthesis/brace and closing parenthesis/brace. + start, end = getLineFoldingRange(pgf, n.Opening, n.Closing, lineFoldingOnly) + + case *ast.GenDecl: + // If this is an import declaration, set the kind to be protocol.Imports. + if n.Tok == token.IMPORT { + kind = protocol.Imports + } + // Fold between positions of or lines between "(" and ")". + start, end = getLineFoldingRange(pgf, n.Lparen, n.Rparen, lineFoldingOnly) + + case *ast.BasicLit: + // Fold raw string literals from position of "`" to position of "`". + if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' { + start, end = n.Pos(), n.End() + } + + case *ast.CompositeLit: + // Fold between positions of or lines between "{" and "}". + start, end = getLineFoldingRange(pgf, n.Lbrace, n.Rbrace, lineFoldingOnly) + + default: + panic(n) } - return true - }) + + // Check that folding positions are valid. + if !start.IsValid() || !end.IsValid() { + continue + } + if start == end { + // Nothing to fold. + continue + } + // in line folding mode, do not fold if the start and end lines are the same. + if lineFoldingOnly && safetoken.Line(pgf.Tok, start) == safetoken.Line(pgf.Tok, end) { + continue + } + rng, err := pgf.PosRange(start, end) + if err != nil { + bug.Reportf("failed to create range: %s", err) // can't happen + continue + } + ranges = append(ranges, foldingRange(kind, rng)) + } // Sort by start position. slices.SortFunc(ranges, func(x, y protocol.FoldingRange) int { @@ -64,66 +136,6 @@ func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, return ranges, nil } -// foldingRangeFunc calculates the line folding range for ast.Node n -func foldingRangeFunc(pgf *parsego.File, n ast.Node, lineFoldingOnly bool) (protocol.FoldingRange, bool) { - // TODO(suzmue): include trailing empty lines before the closing - // parenthesis/brace. - var kind protocol.FoldingRangeKind - // start and end define the range of content to fold away. - var start, end token.Pos - switch n := n.(type) { - case *ast.BlockStmt: - // Fold between positions of or lines between "{" and "}". - start, end = getLineFoldingRange(pgf, n.Lbrace, n.Rbrace, lineFoldingOnly) - case *ast.CaseClause: - // Fold from position of ":" to end. - start, end = n.Colon+1, n.End() - case *ast.CommClause: - // Fold from position of ":" to end. - start, end = n.Colon+1, n.End() - case *ast.CallExpr: - // Fold between positions of or lines between "(" and ")". - start, end = getLineFoldingRange(pgf, n.Lparen, n.Rparen, lineFoldingOnly) - case *ast.FieldList: - // Fold between positions of or lines between opening parenthesis/brace and closing parenthesis/brace. - start, end = getLineFoldingRange(pgf, n.Opening, n.Closing, lineFoldingOnly) - case *ast.GenDecl: - // If this is an import declaration, set the kind to be protocol.Imports. - if n.Tok == token.IMPORT { - kind = protocol.Imports - } - // Fold between positions of or lines between "(" and ")". - start, end = getLineFoldingRange(pgf, n.Lparen, n.Rparen, lineFoldingOnly) - case *ast.BasicLit: - // Fold raw string literals from position of "`" to position of "`". - if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' { - start, end = n.Pos(), n.End() - } - case *ast.CompositeLit: - // Fold between positions of or lines between "{" and "}". - start, end = getLineFoldingRange(pgf, n.Lbrace, n.Rbrace, lineFoldingOnly) - } - - // Check that folding positions are valid. - if !start.IsValid() || !end.IsValid() { - return protocol.FoldingRange{}, false - } - if start == end { - // Nothing to fold. - return protocol.FoldingRange{}, false - } - // in line folding mode, do not fold if the start and end lines are the same. - if lineFoldingOnly && safetoken.Line(pgf.Tok, start) == safetoken.Line(pgf.Tok, end) { - return protocol.FoldingRange{}, false - } - rng, err := pgf.PosRange(start, end) - if err != nil { - bug.Reportf("failed to create range: %s", err) // can't happen - return protocol.FoldingRange{}, false - } - return foldingRange(kind, rng), true -} - // getLineFoldingRange returns the folding range for nodes with parentheses/braces/brackets // that potentially can take up multiple lines. func getLineFoldingRange(pgf *parsego.File, open, close token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) { diff --git a/gopls/internal/golang/implementation.go b/gopls/internal/golang/implementation.go index fe0a34a1c80..a7a7e663d44 100644 --- a/gopls/internal/golang/implementation.go +++ b/gopls/internal/golang/implementation.go @@ -352,17 +352,14 @@ func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *ca var locs []protocol.Location var methodLocs []methodsets.Location for _, pgf := range pkg.CompiledGoFiles() { - ast.Inspect(pgf.File, func(n ast.Node) bool { - spec, ok := n.(*ast.TypeSpec) - if !ok { - return true // not a type declaration - } + for cur := range pgf.Cursor.Preorder((*ast.TypeSpec)(nil)) { + spec := cur.Node().(*ast.TypeSpec) def := pkg.TypesInfo().Defs[spec.Name] if def == nil { - return true // "can't happen" for types + continue // "can't happen" for types } if def.(*types.TypeName).IsAlias() { - return true // skip type aliases to avoid duplicate reporting + continue // skip type aliases to avoid duplicate reporting } candidateType := methodsets.EnsurePointer(def.Type()) @@ -373,20 +370,20 @@ func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *ca // TODO(adonovan): UX: report I/I pairs too? // The same question appears in the global algorithm (methodsets). if !concreteImplementsIntf(&msets, candidateType, queryType) { - return true // not assignable + continue // not assignable } // Ignore types with empty method sets. // (No point reporting that every type satisfies 'any'.) mset := msets.MethodSet(candidateType) if mset.Len() == 0 { - return true + continue } if method == nil { // Found matching type. locs = append(locs, mustLocation(pgf, spec.Name)) - return true + continue } // Find corresponding method. @@ -407,8 +404,7 @@ func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *ca break } } - return true - }) + } } // Finally convert method positions to protocol form by reading the files. diff --git a/gopls/internal/golang/inlay_hint.go b/gopls/internal/golang/inlay_hint.go index bc85745cb0b..84b18e06781 100644 --- a/gopls/internal/golang/inlay_hint.go +++ b/gopls/internal/golang/inlay_hint.go @@ -14,9 +14,11 @@ import ( "strings" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" + "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" @@ -47,7 +49,7 @@ func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pR } info := pkg.TypesInfo() - q := typesinternal.FileQualifier(pgf.File, pkg.Types()) + qual := typesinternal.FileQualifier(pgf.File, pkg.Types()) // Set the range to the full file if the range is not valid. start, end := pgf.File.FileStart, pgf.File.FileEnd @@ -63,20 +65,16 @@ func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pR } var hints []protocol.InlayHint - ast.Inspect(pgf.File, func(node ast.Node) bool { - // If not in range, we can stop looking. - if node == nil || node.End() < start || node.Pos() > end { - return false - } + if curSubrange, ok := pgf.Cursor.FindPos(start, end); ok { + add := func(hint protocol.InlayHint) { hints = append(hints, hint) } for _, fn := range enabledHints { - hints = append(hints, fn(node, pgf.Mapper, pgf.Tok, info, &q)...) + fn(info, pgf, qual, curSubrange, add) } - return true - }) + } return hints, nil } -type inlayHintFunc func(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint +type inlayHintFunc func(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) var allInlayHints = map[settings.InlayHint]inlayHintFunc{ settings.AssignVariableTypes: assignVariableTypes, @@ -88,259 +86,243 @@ var allInlayHints = map[settings.InlayHint]inlayHintFunc{ settings.FunctionTypeParameters: funcTypeParams, } -func parameterNames(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { - callExpr, ok := node.(*ast.CallExpr) - if !ok { - return nil - } - t := info.TypeOf(callExpr.Fun) - if t == nil { - return nil - } - signature, ok := typeparams.CoreType(t).(*types.Signature) - if !ok { - return nil +func parameterNames(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) { + for curCall := range cur.Preorder((*ast.CallExpr)(nil)) { + callExpr := curCall.Node().(*ast.CallExpr) + t := info.TypeOf(callExpr.Fun) + if t == nil { + continue + } + signature, ok := typeparams.CoreType(t).(*types.Signature) + if !ok { + continue + } + + for i, v := range callExpr.Args { + start, err := pgf.PosPosition(v.Pos()) + if err != nil { + continue + } + params := signature.Params() + // When a function has variadic params, we skip args after + // params.Len(). + if i > params.Len()-1 { + break + } + param := params.At(i) + // param.Name is empty for built-ins like append + if param.Name() == "" { + continue + } + // Skip the parameter name hint if the arg matches + // the parameter name. + if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() { + continue + } + + label := param.Name() + if signature.Variadic() && i == params.Len()-1 { + label = label + "..." + } + add(protocol.InlayHint{ + Position: start, + Label: buildLabel(label + ":"), + Kind: protocol.Parameter, + PaddingRight: true, + }) + } } +} - var hints []protocol.InlayHint - for i, v := range callExpr.Args { - start, err := m.PosPosition(tf, v.Pos()) - if err != nil { +func funcTypeParams(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) { + for curCall := range cur.Preorder((*ast.CallExpr)(nil)) { + call := curCall.Node().(*ast.CallExpr) + id, ok := call.Fun.(*ast.Ident) + if !ok { continue } - params := signature.Params() - // When a function has variadic params, we skip args after - // params.Len(). - if i > params.Len()-1 { - break - } - param := params.At(i) - // param.Name is empty for built-ins like append - if param.Name() == "" { + inst := info.Instances[id] + if inst.TypeArgs == nil { continue } - // Skip the parameter name hint if the arg matches - // the parameter name. - if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() { + start, err := pgf.PosPosition(id.End()) + if err != nil { continue } - - label := param.Name() - if signature.Variadic() && i == params.Len()-1 { - label = label + "..." + var args []string + for i := 0; i < inst.TypeArgs.Len(); i++ { + args = append(args, inst.TypeArgs.At(i).String()) + } + if len(args) == 0 { + continue } - hints = append(hints, protocol.InlayHint{ - Position: start, - Label: buildLabel(label + ":"), - Kind: protocol.Parameter, - PaddingRight: true, + add(protocol.InlayHint{ + Position: start, + Label: buildLabel("[" + strings.Join(args, ", ") + "]"), + Kind: protocol.Type, }) } - return hints -} - -func funcTypeParams(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { - ce, ok := node.(*ast.CallExpr) - if !ok { - return nil - } - id, ok := ce.Fun.(*ast.Ident) - if !ok { - return nil - } - inst := info.Instances[id] - if inst.TypeArgs == nil { - return nil - } - start, err := m.PosPosition(tf, id.End()) - if err != nil { - return nil - } - var args []string - for i := 0; i < inst.TypeArgs.Len(); i++ { - args = append(args, inst.TypeArgs.At(i).String()) - } - if len(args) == 0 { - return nil - } - return []protocol.InlayHint{{ - Position: start, - Label: buildLabel("[" + strings.Join(args, ", ") + "]"), - Kind: protocol.Type, - }} } -func assignVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { - stmt, ok := node.(*ast.AssignStmt) - if !ok || stmt.Tok != token.DEFINE { - return nil - } - - var hints []protocol.InlayHint - for _, v := range stmt.Lhs { - if h := variableType(v, m, tf, info, q); h != nil { - hints = append(hints, *h) +func assignVariableTypes(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) { + for curAssign := range cur.Preorder((*ast.AssignStmt)(nil)) { + stmt := curAssign.Node().(*ast.AssignStmt) + if stmt.Tok != token.DEFINE { + continue + } + for _, v := range stmt.Lhs { + variableType(info, pgf, qual, v, add) } } - return hints } -func rangeVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { - rStmt, ok := node.(*ast.RangeStmt) - if !ok { - return nil - } - var hints []protocol.InlayHint - if h := variableType(rStmt.Key, m, tf, info, q); h != nil { - hints = append(hints, *h) - } - if h := variableType(rStmt.Value, m, tf, info, q); h != nil { - hints = append(hints, *h) +func rangeVariableTypes(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) { + for curRange := range cur.Preorder((*ast.RangeStmt)(nil)) { + rStmt := curRange.Node().(*ast.RangeStmt) + variableType(info, pgf, qual, rStmt.Key, add) + variableType(info, pgf, qual, rStmt.Value, add) } - return hints } -func variableType(e ast.Expr, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) *protocol.InlayHint { +func variableType(info *types.Info, pgf *parsego.File, qual types.Qualifier, e ast.Expr, add func(protocol.InlayHint)) { typ := info.TypeOf(e) if typ == nil { - return nil + return } - end, err := m.PosPosition(tf, e.End()) + end, err := pgf.PosPosition(e.End()) if err != nil { - return nil + return } - return &protocol.InlayHint{ + add(protocol.InlayHint{ Position: end, - Label: buildLabel(types.TypeString(typ, *q)), + Label: buildLabel(types.TypeString(typ, qual)), Kind: protocol.Type, PaddingLeft: true, - } + }) } -func constantValues(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { - genDecl, ok := node.(*ast.GenDecl) - if !ok || genDecl.Tok != token.CONST { - return nil - } - - var hints []protocol.InlayHint - for _, v := range genDecl.Specs { - spec, ok := v.(*ast.ValueSpec) - if !ok { +func constantValues(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) { + for curDecl := range cur.Preorder((*ast.GenDecl)(nil)) { + genDecl := curDecl.Node().(*ast.GenDecl) + if genDecl.Tok != token.CONST { continue } - end, err := m.PosPosition(tf, v.End()) - if err != nil { - continue - } - // Show hints when values are missing or at least one value is not - // a basic literal. - showHints := len(spec.Values) == 0 - checkValues := len(spec.Names) == len(spec.Values) - var values []string - for i, w := range spec.Names { - obj, ok := info.ObjectOf(w).(*types.Const) - if !ok || obj.Val().Kind() == constant.Unknown { - return nil + + for _, v := range genDecl.Specs { + spec, ok := v.(*ast.ValueSpec) + if !ok { + continue + } + end, err := pgf.PosPosition(v.End()) + if err != nil { + continue } - if checkValues { - switch spec.Values[i].(type) { - case *ast.BadExpr: - return nil - case *ast.BasicLit: - default: - if obj.Val().Kind() != constant.Bool { - showHints = true + // Show hints when values are missing or at least one value is not + // a basic literal. + showHints := len(spec.Values) == 0 + checkValues := len(spec.Names) == len(spec.Values) + var values []string + for i, w := range spec.Names { + obj, ok := info.ObjectOf(w).(*types.Const) + if !ok || obj.Val().Kind() == constant.Unknown { + continue + } + if checkValues { + switch spec.Values[i].(type) { + case *ast.BadExpr: + continue + case *ast.BasicLit: + default: + if obj.Val().Kind() != constant.Bool { + showHints = true + } } } + values = append(values, fmt.Sprintf("%v", obj.Val())) } - values = append(values, fmt.Sprintf("%v", obj.Val())) - } - if !showHints || len(values) == 0 { - continue + if !showHints || len(values) == 0 { + continue + } + add(protocol.InlayHint{ + Position: end, + Label: buildLabel("= " + strings.Join(values, ", ")), + PaddingLeft: true, + }) } - hints = append(hints, protocol.InlayHint{ - Position: end, - Label: buildLabel("= " + strings.Join(values, ", ")), - PaddingLeft: true, - }) } - return hints } -func compositeLiteralFields(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { - compLit, ok := node.(*ast.CompositeLit) - if !ok { - return nil - } - typ := info.TypeOf(compLit) - if typ == nil { - return nil - } - typ = typesinternal.Unpointer(typ) - strct, ok := typeparams.CoreType(typ).(*types.Struct) - if !ok { - return nil - } +func compositeLiteralFields(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) { + for curCompLit := range cur.Preorder((*ast.CompositeLit)(nil)) { + compLit, ok := curCompLit.Node().(*ast.CompositeLit) + typ := info.TypeOf(compLit) + if typ == nil { + continue + } + typ = typesinternal.Unpointer(typ) + strct, ok := typeparams.CoreType(typ).(*types.Struct) + if !ok { + continue + } - var hints []protocol.InlayHint - var allEdits []protocol.TextEdit - for i, v := range compLit.Elts { - if _, ok := v.(*ast.KeyValueExpr); !ok { - start, err := m.PosPosition(tf, v.Pos()) - if err != nil { - continue - } - if i > strct.NumFields()-1 { - break + var hints []protocol.InlayHint + var allEdits []protocol.TextEdit + for i, v := range compLit.Elts { + if _, ok := v.(*ast.KeyValueExpr); !ok { + start, err := pgf.PosPosition(v.Pos()) + if err != nil { + continue + } + if i > strct.NumFields()-1 { + break + } + hints = append(hints, protocol.InlayHint{ + Position: start, + Label: buildLabel(strct.Field(i).Name() + ":"), + Kind: protocol.Parameter, + PaddingRight: true, + }) + allEdits = append(allEdits, protocol.TextEdit{ + Range: protocol.Range{Start: start, End: start}, + NewText: strct.Field(i).Name() + ": ", + }) } - hints = append(hints, protocol.InlayHint{ - Position: start, - Label: buildLabel(strct.Field(i).Name() + ":"), - Kind: protocol.Parameter, - PaddingRight: true, - }) - allEdits = append(allEdits, protocol.TextEdit{ - Range: protocol.Range{Start: start, End: start}, - NewText: strct.Field(i).Name() + ": ", - }) + } + // It is not allowed to have a mix of keyed and unkeyed fields, so + // have the text edits add keys to all fields. + for i := range hints { + hints[i].TextEdits = allEdits + add(hints[i]) } } - // It is not allowed to have a mix of keyed and unkeyed fields, so - // have the text edits add keys to all fields. - for i := range hints { - hints[i].TextEdits = allEdits - } - return hints } -func compositeLiteralTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { - compLit, ok := node.(*ast.CompositeLit) - if !ok { - return nil - } - typ := info.TypeOf(compLit) - if typ == nil { - return nil - } - if compLit.Type != nil { - return nil - } - prefix := "" - if t, ok := typeparams.CoreType(typ).(*types.Pointer); ok { - typ = t.Elem() - prefix = "&" - } - // The type for this composite literal is implicit, add an inlay hint. - start, err := m.PosPosition(tf, compLit.Lbrace) - if err != nil { - return nil +func compositeLiteralTypes(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) { + for curCompLit := range cur.Preorder((*ast.CompositeLit)(nil)) { + compLit := curCompLit.Node().(*ast.CompositeLit) + typ := info.TypeOf(compLit) + if typ == nil { + continue + } + if compLit.Type != nil { + continue + } + prefix := "" + if t, ok := typeparams.CoreType(typ).(*types.Pointer); ok { + typ = t.Elem() + prefix = "&" + } + // The type for this composite literal is implicit, add an inlay hint. + start, err := pgf.PosPosition(compLit.Lbrace) + if err != nil { + continue + } + add(protocol.InlayHint{ + Position: start, + Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, qual))), + Kind: protocol.Type, + }) } - return []protocol.InlayHint{{ - Position: start, - Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))), - Kind: protocol.Type, - }} } func buildLabel(s string) []protocol.InlayHintLabelPart { diff --git a/gopls/internal/golang/references.go b/gopls/internal/golang/references.go index 3ecaab6e3e1..12152453dcd 100644 --- a/gopls/internal/golang/references.go +++ b/gopls/internal/golang/references.go @@ -602,14 +602,12 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo // Scan through syntax looking for uses of one of the target objects. for _, pgf := range pkg.CompiledGoFiles() { - ast.Inspect(pgf.File, func(n ast.Node) bool { - if id, ok := n.(*ast.Ident); ok { - if obj, ok := pkg.TypesInfo().Uses[id]; ok && matches(obj) { - report(mustLocation(pgf, id), false) - } + for curId := range pgf.Cursor.Preorder((*ast.Ident)(nil)) { + id := curId.Node().(*ast.Ident) + if obj, ok := pkg.TypesInfo().Uses[id]; ok && matches(obj) { + report(mustLocation(pgf, id), false) } - return true - }) + } } return nil } diff --git a/gopls/internal/server/link.go b/gopls/internal/server/link.go index 13097d89887..c888904baab 100644 --- a/gopls/internal/server/link.go +++ b/gopls/internal/server/link.go @@ -164,17 +164,15 @@ func goLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]p // Gather links found in string literals. var str []*ast.BasicLit - ast.Inspect(pgf.File, func(node ast.Node) bool { - switch n := node.(type) { - case *ast.ImportSpec: - return false // don't process import strings again - case *ast.BasicLit: - if n.Kind == token.STRING { - str = append(str, n) + for curLit := range pgf.Cursor.Preorder((*ast.BasicLit)(nil)) { + lit := curLit.Node().(*ast.BasicLit) + if lit.Kind == token.STRING { + if _, ok := curLit.Parent().Node().(*ast.ImportSpec); ok { + continue // ignore import strings } + str = append(str, lit) } - return true - }) + } for _, s := range str { strOffset, err := safetoken.Offset(pgf.Tok, s.Pos()) if err != nil { diff --git a/internal/astutil/cursor/cursor.go b/internal/astutil/cursor/cursor.go index 1052f65acfb..5ed177c9f3d 100644 --- a/internal/astutil/cursor/cursor.go +++ b/internal/astutil/cursor/cursor.go @@ -407,6 +407,8 @@ func (c Cursor) FindNode(n ast.Node) (Cursor, bool) { // FindPos returns the cursor for the innermost node n in the tree // rooted at c such that n.Pos() <= start && end <= n.End(). +// (For an *ast.File, it uses the bounds n.FileStart-n.FileEnd.) +// // It returns zero if none is found. // Precondition: start <= end. // @@ -425,10 +427,16 @@ func (c Cursor) FindPos(start, end token.Pos) (Cursor, bool) { for i, limit := c.indices(); i < limit; i++ { ev := events[i] if ev.index > i { // push? - if ev.node.Pos() > start { + n := ev.node + var nodeStart, nodeEnd token.Pos + if file, ok := n.(*ast.File); ok { + nodeStart, nodeEnd = file.FileStart, file.FileEnd + } else { + nodeStart, nodeEnd = n.Pos(), n.End() + } + if nodeStart > start { break // disjoint, after; stop } - nodeEnd := ev.node.End() if end <= nodeEnd { // node fully contains target range best = i From 3c245dad2c55e3b6e3e1635f1d5bc3da5277be83 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 19 Feb 2025 15:19:16 +0700 Subject: [PATCH 36/99] gopls: fix diagnostics integration test CL 649355 improved simplifyrange suggested fix, thus the corresponding diagnostics integration test must be updated, too. Change-Id: Ief33cc4e9ab3d760c1af28c94102638f6d2b69e8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650556 Reviewed-by: Michael Knyszek Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Cuong Manh Le Reviewed-by: Alan Donovan --- gopls/internal/test/integration/diagnostics/diagnostics_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/test/integration/diagnostics/diagnostics_test.go b/gopls/internal/test/integration/diagnostics/diagnostics_test.go index c496f6464a3..a97d249e7b5 100644 --- a/gopls/internal/test/integration/diagnostics/diagnostics_test.go +++ b/gopls/internal/test/integration/diagnostics/diagnostics_test.go @@ -562,7 +562,7 @@ func _() { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - Diagnostics(AtPosition("main.go", 5, 8)), + Diagnostics(AtPosition("main.go", 5, 6)), ReadDiagnostics("main.go", &d), ) if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { From 44abb0ac312034f5c655a3d815ec385884038027 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Fri, 14 Feb 2025 10:16:33 -0500 Subject: [PATCH 37/99] go/types/internal/play: display type structure Display the complete recursive structure of a types.Type, in the style of ast.Fprint. Change-Id: I408c9b1f1beb214e0184381e97085e606ad8a5a1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649617 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/types/internal/play/play.go | 66 +++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go index eb9e5794b94..8d3b9d19346 100644 --- a/go/types/internal/play/play.go +++ b/go/types/internal/play/play.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.23 + // The play program is a playground for go/types: a simple web-based // text editor into which the user can enter a Go program, select a // region, and see type information about it. @@ -35,7 +37,6 @@ import ( // TODO(adonovan): // - show line numbers next to textarea. -// - show a (tree) breakdown of the representation of the expression's type. // - mention this in the go/types tutorial. // - display versions of go/types and go command. @@ -297,6 +298,10 @@ func formatObj(out *strings.Builder, fset *token.FileSet, ref string, obj types. } fmt.Fprintf(out, "\n\n") + fmt.Fprintf(out, "Type:\n") + describeType(out, obj.Type()) + fmt.Fprintf(out, "\n") + // method set if methods := typeutil.IntuitiveMethodSet(obj.Type(), nil); len(methods) > 0 { fmt.Fprintf(out, "Methods:\n") @@ -318,6 +323,65 @@ func formatObj(out *strings.Builder, fset *token.FileSet, ref string, obj types. } } +// describeType formats t to out in a way that makes it clear what methods to call on t to +// get at its parts. +// describeType assumes t was constructed by the type checker, so it doesn't check +// for recursion. The type checker replaces recursive alias types, which are illegal, +// with a BasicType that says as much. Other types that it constructs are recursive +// only via a name, and this function does not traverse names. +func describeType(out *strings.Builder, t types.Type) { + depth := -1 + + var ft func(string, types.Type) + ft = func(prefix string, t types.Type) { + depth++ + defer func() { depth-- }() + + for range depth { + fmt.Fprint(out, ". ") + } + + fmt.Fprintf(out, "%s%T:", prefix, t) + switch t := t.(type) { + case *types.Basic: + fmt.Fprintf(out, " Name: %q\n", t.Name()) + case *types.Pointer: + fmt.Fprintln(out) + ft("Elem: ", t.Elem()) + case *types.Slice: + fmt.Fprintln(out) + ft("Elem: ", t.Elem()) + case *types.Array: + fmt.Fprintf(out, " Len: %d\n", t.Len()) + ft("Elem: ", t.Elem()) + case *types.Map: + fmt.Fprintln(out) + ft("Key: ", t.Key()) + ft("Elem: ", t.Elem()) + case *types.Chan: + fmt.Fprintf(out, " Dir: %s\n", chanDirs[t.Dir()]) + ft("Elem: ", t.Elem()) + case *types.Alias: + fmt.Fprintf(out, " Name: %q\n", t.Obj().Name()) + ft("Rhs: ", t.Rhs()) + default: + // For types we may have missed or which have too much to bother with, + // print their string representation. + // TODO(jba): print more about struct types (their fields) and interface and named + // types (their methods). + fmt.Fprintf(out, " %s\n", t) + } + } + + ft("", t) +} + +var chanDirs = []string{ + "SendRecv", + "SendOnly", + "RecvOnly", +} + func handleRoot(w http.ResponseWriter, req *http.Request) { io.WriteString(w, mainHTML) } func handleJS(w http.ResponseWriter, req *http.Request) { io.WriteString(w, mainJS) } func handleCSS(w http.ResponseWriter, req *http.Request) { io.WriteString(w, mainCSS) } From 877c1d128ba89b146547e8becff924a35cb9b322 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 19 Feb 2025 17:50:27 +0000 Subject: [PATCH 38/99] gopls: address various staticcheck findings Fix several real findings uncovered by running staticcheck ./... from the gopls module. Change-Id: Ieffd38bfd98cac24052c3b408907eb197c9e1cda Reviewed-on: https://go-review.googlesource.com/c/tools/+/650643 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- .../internal/analysis/modernize/sortslice.go | 5 ++-- .../analysis/unusedvariable/unusedvariable.go | 2 +- gopls/internal/cache/analysis.go | 3 +-- gopls/internal/cache/check.go | 23 +------------------ .../cache/methodsets/fingerprint_test.go | 5 ---- gopls/internal/cache/snapshot.go | 2 +- gopls/internal/cache/source.go | 13 ++++++----- gopls/internal/cache/view.go | 3 --- gopls/internal/cmd/cmd.go | 9 ++------ gopls/internal/golang/extract.go | 2 +- gopls/internal/golang/inlay_hint.go | 3 +++ gopls/internal/golang/semtok.go | 2 +- gopls/internal/golang/workspace_symbol.go | 2 -- gopls/internal/settings/settings.go | 8 +++---- 14 files changed, 24 insertions(+), 58 deletions(-) diff --git a/gopls/internal/analysis/modernize/sortslice.go b/gopls/internal/analysis/modernize/sortslice.go index bbc8befb8ee..a033be7f635 100644 --- a/gopls/internal/analysis/modernize/sortslice.go +++ b/gopls/internal/analysis/modernize/sortslice.go @@ -5,7 +5,6 @@ package modernize import ( - "fmt" "go/ast" "go/token" "go/types" @@ -78,9 +77,9 @@ func sortslice(pass *analysis.Pass) { Pos: call.Fun.Pos(), End: call.Fun.End(), Category: "sortslice", - Message: fmt.Sprintf("sort.Slice can be modernized using slices.Sort"), + Message: "sort.Slice can be modernized using slices.Sort", SuggestedFixes: []analysis.SuggestedFix{{ - Message: fmt.Sprintf("Replace sort.Slice call by slices.Sort"), + Message: "Replace sort.Slice call by slices.Sort", TextEdits: append(importEdits, []analysis.TextEdit{ { // Replace sort.Slice with slices.Sort. diff --git a/gopls/internal/analysis/unusedvariable/unusedvariable.go b/gopls/internal/analysis/unusedvariable/unusedvariable.go index 5f1c188eb6a..3ea1dbe6953 100644 --- a/gopls/internal/analysis/unusedvariable/unusedvariable.go +++ b/gopls/internal/analysis/unusedvariable/unusedvariable.go @@ -47,7 +47,7 @@ func run(pass *analysis.Pass) (any, error) { if len(match) > 0 { varName := match[1] // Beginning in Go 1.23, go/types began quoting vars as `v'. - varName = strings.Trim(varName, "'`'") + varName = strings.Trim(varName, "`'") err := runForError(pass, typeErr, varName) if err != nil { diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go index d570c0a46ae..a0dd322a51e 100644 --- a/gopls/internal/cache/analysis.go +++ b/gopls/internal/cache/analysis.go @@ -822,8 +822,7 @@ func typesLookup(pkg *types.Package) func(string) *types.Package { ) // search scans children the next package in pending, looking for pkgPath. - var search func(pkgPath string) (*types.Package, int) - search = func(pkgPath string) (sought *types.Package, numPending int) { + search := func(pkgPath string) (sought *types.Package, numPending int) { mu.Lock() defer mu.Unlock() diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index a3aff5e5475..aa1537c8705 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -44,11 +44,6 @@ import ( "golang.org/x/tools/internal/versions" ) -// Various optimizations that should not affect correctness. -const ( - preserveImportGraph = true // hold on to the import graph for open packages -) - type unit = struct{} // A typeCheckBatch holds data for a logical type-checking operation, which may @@ -97,21 +92,6 @@ func (b *typeCheckBatch) getHandle(id PackageID) *packageHandle { return b._handles[id] } -// A futurePackage is a future result of type checking or importing a package, -// to be cached in a map. -// -// The goroutine that creates the futurePackage is responsible for evaluating -// its value, and closing the done channel. -type futurePackage struct { - done chan unit - v pkgOrErr -} - -type pkgOrErr struct { - pkg *types.Package - err error -} - // TypeCheck parses and type-checks the specified packages, // and returns them in the same order as the ids. // The resulting packages' types may belong to different importers, @@ -701,8 +681,7 @@ func importLookup(mp *metadata.Package, source metadata.Source) func(PackagePath // search scans children the next package in pending, looking for pkgPath. // Invariant: whenever search is called, pkgPath is not yet mapped. - var search func(pkgPath PackagePath) (PackageID, bool) - search = func(pkgPath PackagePath) (id PackageID, found bool) { + search := func(pkgPath PackagePath) (id PackageID, found bool) { pkg := pending[0] pending = pending[1:] for depPath, depID := range pkg.DepsByPkgPath { diff --git a/gopls/internal/cache/methodsets/fingerprint_test.go b/gopls/internal/cache/methodsets/fingerprint_test.go index a9f47c1a2e6..795ddaa965b 100644 --- a/gopls/internal/cache/methodsets/fingerprint_test.go +++ b/gopls/internal/cache/methodsets/fingerprint_test.go @@ -39,11 +39,6 @@ func Test_fingerprint(t *testing.T) { // (Non-tricky types only.) var fingerprints typeutil.Map - type eqclass struct { - class map[types.Type]bool - fp string - } - for _, pkg := range pkgs { switch pkg.Types.Path() { case "unsafe", "builtin": diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index c341ac6e85a..578cea61eb7 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -1301,7 +1301,7 @@ searchOverlays: // where the file is inside a workspace module, but perhaps no packages // were loaded for that module. _, loadedMod := loadedModFiles[goMod] - _, workspaceMod := s.view.viewDefinition.workspaceModFiles[goMod] + _, workspaceMod := s.view.workspaceModFiles[goMod] // If we have a relevant go.mod file, check whether the file is orphaned // due to its go.mod file being inactive. We could also offer a // prescriptive diagnostic in the case that there is no go.mod file, but diff --git a/gopls/internal/cache/source.go b/gopls/internal/cache/source.go index 3e21c641651..fa038ec37a6 100644 --- a/gopls/internal/cache/source.go +++ b/gopls/internal/cache/source.go @@ -61,9 +61,7 @@ func (s *goplsSource) ResolveReferences(ctx context.Context, filename string, mi // collect the ones that are still needed := maps.Clone(missing) for _, a := range fromWS { - if _, ok := needed[a.Package.Name]; ok { - delete(needed, a.Package.Name) - } + delete(needed, a.Package.Name) } // when debug (below) is gone, change this to: if len(needed) == 0 {return fromWS, nil} var fromCache []*result @@ -184,10 +182,13 @@ type found struct { func (s *goplsSource) resolveWorkspaceReferences(filename string, missing imports.References) ([]*imports.Result, error) { uri := protocol.URIFromPath(filename) mypkgs, err := s.S.MetadataForFile(s.ctx, uri) - if len(mypkgs) != 1 { - // what does this mean? can it happen? + if err != nil { + return nil, err + } + if len(mypkgs) == 0 { + return nil, nil } - mypkg := mypkgs[0] + mypkg := mypkgs[0] // narrowest package // search the metadata graph for package ids correstponding to missing g := s.S.MetadataGraph() var ids []metadata.PackageID diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go index 6ebf6837ef2..fc1ac5724ed 100644 --- a/gopls/internal/cache/view.go +++ b/gopls/internal/cache/view.go @@ -20,7 +20,6 @@ import ( "os/exec" "path" "path/filepath" - "regexp" "slices" "sort" "strings" @@ -1253,8 +1252,6 @@ func globsMatchPath(globs, target string) bool { return false } -var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`) - // TODO(rfindley): clean up the redundancy of allFilesExcluded, // pathExcludedByFilterFunc, pathExcludedByFilter, view.filterFunc... func allFilesExcluded(files []string, filterFunc func(protocol.DocumentURI) bool) bool { diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go index a647b3198df..f7ba04df6a4 100644 --- a/gopls/internal/cmd/cmd.go +++ b/gopls/internal/cmd/cmd.go @@ -310,11 +310,6 @@ func (app *Application) featureCommands() []tool.Application { } } -var ( - internalMu sync.Mutex - internalConnections = make(map[string]*connection) -) - // connect creates and initializes a new in-process gopls session. func (app *Application) connect(ctx context.Context) (*connection, error) { client := newClient(app) @@ -377,10 +372,10 @@ func (c *connection) initialize(ctx context.Context, options func(*settings.Opti params.InitializationOptions = map[string]interface{}{ "symbolMatcher": string(opts.SymbolMatcher), } - if c.initializeResult, err = c.Server.Initialize(ctx, params); err != nil { + if c.initializeResult, err = c.Initialize(ctx, params); err != nil { return err } - if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { + if err := c.Initialized(ctx, &protocol.InitializedParams{}); err != nil { return err } return nil diff --git a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go index 8c8758d9f0a..b8219562de5 100644 --- a/gopls/internal/golang/extract.go +++ b/gopls/internal/golang/extract.go @@ -375,7 +375,7 @@ func stmtToInsertVarBefore(path []ast.Node, variables []*variable) (ast.Stmt, er } return parent, nil } - return enclosingStmt.(ast.Stmt), nil + return enclosingStmt, nil } // canExtractVariable reports whether the code in the given range can be diff --git a/gopls/internal/golang/inlay_hint.go b/gopls/internal/golang/inlay_hint.go index 84b18e06781..b49ebd85e21 100644 --- a/gopls/internal/golang/inlay_hint.go +++ b/gopls/internal/golang/inlay_hint.go @@ -255,6 +255,9 @@ func constantValues(info *types.Info, pgf *parsego.File, qual types.Qualifier, c func compositeLiteralFields(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) { for curCompLit := range cur.Preorder((*ast.CompositeLit)(nil)) { compLit, ok := curCompLit.Node().(*ast.CompositeLit) + if !ok { + continue + } typ := info.TypeOf(compLit) if typ == nil { continue diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go index cb3f2cfd478..121531d8280 100644 --- a/gopls/internal/golang/semtok.go +++ b/gopls/internal/golang/semtok.go @@ -616,7 +616,7 @@ func (tv *tokenVisitor) ident(id *ast.Ident) { obj types.Object ok bool ) - if obj, ok = tv.info.Defs[id]; obj != nil { + if obj, _ = tv.info.Defs[id]; obj != nil { // definition mods = append(mods, semtok.ModDefinition) tok, mods = tv.appendObjectModifiers(mods, obj) diff --git a/gopls/internal/golang/workspace_symbol.go b/gopls/internal/golang/workspace_symbol.go index feba6081515..89c144b9230 100644 --- a/gopls/internal/golang/workspace_symbol.go +++ b/gopls/internal/golang/workspace_symbol.go @@ -293,14 +293,12 @@ func (c comboMatcher) match(chunks []string) (int, float64) { func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherType settings.SymbolMatcher, symbolizer symbolizer, query string) ([]protocol.SymbolInformation, error) { // Extract symbols from all files. var work []symbolFile - var roots []string seen := make(map[protocol.DocumentURI]*metadata.Package) // only scan each file once for _, snapshot := range snapshots { // Use the root view URIs for determining (lexically) // whether a URI is in any open workspace. folderURI := snapshot.Folder() - roots = append(roots, strings.TrimRight(string(folderURI), "/")) filters := snapshot.Options().DirectoryFilters filterer := cache.NewFilterer(filters) diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 7d64cbef459..393bccac312 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -740,8 +740,8 @@ type ImportsSourceEnum string const ( ImportsSourceOff ImportsSourceEnum = "off" - ImportsSourceGopls = "gopls" - ImportsSourceGoimports = "goimports" + ImportsSourceGopls ImportsSourceEnum = "gopls" + ImportsSourceGoimports ImportsSourceEnum = "goimports" ) type Matcher string @@ -967,7 +967,7 @@ func validateDirectoryFilter(ifilter string) (string, error) { if seg != "**" { for _, op := range unsupportedOps { if strings.Contains(seg, op) { - return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue.", filter, op) + return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue", filter, op) } } } @@ -1374,7 +1374,7 @@ func (e *SoftError) Error() string { // deprecatedError reports the current setting as deprecated. // The optional replacement is suggested to the user. func deprecatedError(replacement string) error { - msg := fmt.Sprintf("this setting is deprecated") + msg := "this setting is deprecated" if replacement != "" { msg = fmt.Sprintf("%s, use %q instead", msg, replacement) } From 4991e7daac565a1cb14d843e78a63a1a91f726d4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 18 Feb 2025 16:04:35 -0500 Subject: [PATCH 39/99] gopls/internal/golang: use pgf.Cursor in CodeAction fix This CL pushes down the pgf.Cursor into internal interfaces so that it is avaiable where needed. The existing implementations have not been updated to use it. As a rule of thumb, any place that calls PathEnclosingInterval would be better off using Cursor; the exception is when there's a public API that accepts a 'path []Node'. Change-Id: I18313809808fa83cc1f2a1d51850a9fdcf43ecdb Reviewed-on: https://go-review.googlesource.com/c/tools/+/650398 LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Reviewed-by: Robert Findley Commit-Queue: Alan Donovan --- .../analysis/fillstruct/fillstruct.go | 7 +-- gopls/internal/golang/codeaction.go | 20 ++++----- gopls/internal/golang/extract.go | 44 ++++++++++++------- gopls/internal/golang/fix.go | 6 +-- gopls/internal/golang/invertifcondition.go | 9 ++-- gopls/internal/golang/lines.go | 22 ++++++---- gopls/internal/golang/stub.go | 6 +-- .../golang/stubmethods/stubcalledfunc.go | 6 ++- .../golang/stubmethods/stubmethods.go | 9 +++- gopls/internal/golang/undeclared.go | 5 ++- gopls/internal/util/typesutil/typesutil.go | 2 + 11 files changed, 84 insertions(+), 52 deletions(-) diff --git a/gopls/internal/analysis/fillstruct/fillstruct.go b/gopls/internal/analysis/fillstruct/fillstruct.go index a8a861f0651..641b98e6fa7 100644 --- a/gopls/internal/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/analysis/fillstruct/fillstruct.go @@ -28,6 +28,7 @@ import ( "golang.org/x/tools/gopls/internal/fuzzy" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" ) @@ -129,15 +130,15 @@ const FixCategory = "fillstruct" // recognized by gopls ApplyFix // SuggestedFix computes the suggested fix for the kinds of // diagnostics produced by the Analyzer above. -func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { if info == nil { return nil, nil, fmt.Errorf("nil types.Info") } pos := start // don't use the end - // TODO(rstambler): Using ast.Inspect would probably be more efficient than - // calling PathEnclosingInterval. Switch this approach. + // TODO(adonovan): simplify, using Cursor. + file := curFile.Node().(*ast.File) path, _ := astutil.PathEnclosingInterval(file, pos, pos) if len(path) == 0 { return nil, nil, fmt.Errorf("no enclosing ast.Node") diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index f82c32d6a9c..49a861852ff 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -325,8 +325,7 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error { case strings.Contains(msg, "missing method"), strings.HasPrefix(msg, "cannot convert"), strings.Contains(msg, "not implement"): - path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end) - si := stubmethods.GetIfaceStubInfo(req.pkg.FileSet(), info, path, start) + si := stubmethods.GetIfaceStubInfo(req.pkg.FileSet(), info, req.pgf, start, end) if si != nil { qual := typesinternal.FileQualifier(req.pgf.File, si.Concrete.Obj().Pkg()) iface := types.TypeString(si.Interface.Type(), qual) @@ -338,8 +337,7 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error { // Offer a "Declare missing method T.f" code action. // See [stubMissingCalledFunctionFixer] for command implementation. case strings.Contains(msg, "has no field or method"): - path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end) - si := stubmethods.GetCallStubInfo(req.pkg.FileSet(), info, path, start) + si := stubmethods.GetCallStubInfo(req.pkg.FileSet(), info, req.pgf, start, end) if si != nil { msg := fmt.Sprintf("Declare missing method %s.%s", si.Receiver.Obj().Name(), si.MethodName) req.addApplyFixAction(msg, fixMissingCalledFunction, req.loc) @@ -462,7 +460,7 @@ func goDoc(ctx context.Context, req *codeActionsRequest) error { // refactorExtractFunction produces "Extract function" code actions. // See [extractFunction] for command implementation. func refactorExtractFunction(ctx context.Context, req *codeActionsRequest) error { - if _, ok, _, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok { + if _, ok, _, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.Cursor); ok { req.addApplyFixAction("Extract function", fixExtractFunction, req.loc) } return nil @@ -471,7 +469,7 @@ func refactorExtractFunction(ctx context.Context, req *codeActionsRequest) error // refactorExtractMethod produces "Extract method" code actions. // See [extractMethod] for command implementation. func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error { - if _, ok, methodOK, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok && methodOK { + if _, ok, methodOK, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.Cursor); ok && methodOK { req.addApplyFixAction("Extract method", fixExtractMethod, req.loc) } return nil @@ -481,7 +479,7 @@ func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error { // See [extractVariable] for command implementation. func refactorExtractVariable(ctx context.Context, req *codeActionsRequest) error { info := req.pkg.TypesInfo() - if exprs, err := canExtractVariable(info, req.pgf.File, req.start, req.end, false); err == nil { + if exprs, err := canExtractVariable(info, req.pgf.Cursor, req.start, req.end, false); err == nil { // Offer one of refactor.extract.{constant,variable} // based on the constness of the expression; this is a // limitation of the codeActionProducers mechanism. @@ -507,7 +505,7 @@ func refactorExtractVariableAll(ctx context.Context, req *codeActionsRequest) er info := req.pkg.TypesInfo() // Don't suggest if only one expr is found, // otherwise it will duplicate with [refactorExtractVariable] - if exprs, err := canExtractVariable(info, req.pgf.File, req.start, req.end, true); err == nil && len(exprs) > 1 { + if exprs, err := canExtractVariable(info, req.pgf.Cursor, req.start, req.end, true); err == nil && len(exprs) > 1 { start, end, err := req.pgf.NodeOffsets(exprs[0]) if err != nil { return err @@ -664,7 +662,7 @@ func refactorRewriteChangeQuote(ctx context.Context, req *codeActionsRequest) er // refactorRewriteInvertIf produces "Invert 'if' condition" code actions. // See [invertIfCondition] for command implementation. func refactorRewriteInvertIf(ctx context.Context, req *codeActionsRequest) error { - if _, ok, _ := canInvertIfCondition(req.pgf.File, req.start, req.end); ok { + if _, ok, _ := canInvertIfCondition(req.pgf.Cursor, req.start, req.end); ok { req.addApplyFixAction("Invert 'if' condition", fixInvertIfCondition, req.loc) } return nil @@ -674,7 +672,7 @@ func refactorRewriteInvertIf(ctx context.Context, req *codeActionsRequest) error // See [splitLines] for command implementation. func refactorRewriteSplitLines(ctx context.Context, req *codeActionsRequest) error { // TODO(adonovan): opt: don't set needPkg just for FileSet. - if msg, ok, _ := canSplitLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok { + if msg, ok, _ := canSplitLines(req.pgf.Cursor, req.pkg.FileSet(), req.start, req.end); ok { req.addApplyFixAction(msg, fixSplitLines, req.loc) } return nil @@ -684,7 +682,7 @@ func refactorRewriteSplitLines(ctx context.Context, req *codeActionsRequest) err // See [joinLines] for command implementation. func refactorRewriteJoinLines(ctx context.Context, req *codeActionsRequest) error { // TODO(adonovan): opt: don't set needPkg just for FileSet. - if msg, ok, _ := canJoinLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok { + if msg, ok, _ := canJoinLines(req.pgf.Cursor, req.pkg.FileSet(), req.start, req.end); ok { req.addApplyFixAction(msg, fixJoinLines, req.loc) } return nil diff --git a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go index b8219562de5..3d2b880db2d 100644 --- a/gopls/internal/golang/extract.go +++ b/gopls/internal/golang/extract.go @@ -24,17 +24,18 @@ import ( "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/typesinternal" ) // extractVariable implements the refactor.extract.{variable,constant} CodeAction command. -func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - return extractExprs(fset, start, end, src, file, info, false) +func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, _ *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + return extractExprs(fset, start, end, src, curFile, info, false) } // extractVariableAll implements the refactor.extract.{variable,constant}-all CodeAction command. -func extractVariableAll(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - return extractExprs(fset, start, end, src, file, info, true) +func extractVariableAll(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, _ *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + return extractExprs(fset, start, end, src, curFile, info, true) } // extractExprs replaces occurrence(s) of a specified expression within the same function @@ -43,9 +44,11 @@ func extractVariableAll(fset *token.FileSet, start, end token.Pos, src []byte, f // // The new variable/constant is declared as close as possible to the first found expression // within the deepest common scope accessible to all candidate occurrences. -func extractExprs(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, info *types.Info, all bool) (*token.FileSet, *analysis.SuggestedFix, error) { +func extractExprs(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, info *types.Info, all bool) (*token.FileSet, *analysis.SuggestedFix, error) { + file := curFile.Node().(*ast.File) + // TODO(adonovan): simplify, using Cursor. tokFile := fset.File(file.FileStart) - exprs, err := canExtractVariable(info, file, start, end, all) + exprs, err := canExtractVariable(info, curFile, start, end, all) if err != nil { return nil, nil, fmt.Errorf("cannot extract: %v", err) } @@ -381,10 +384,12 @@ func stmtToInsertVarBefore(path []ast.Node, variables []*variable) (ast.Stmt, er // canExtractVariable reports whether the code in the given range can be // extracted to a variable (or constant). It returns the selected expression or, if 'all', // all structurally equivalent expressions within the same function body, in lexical order. -func canExtractVariable(info *types.Info, file *ast.File, start, end token.Pos, all bool) ([]ast.Expr, error) { +func canExtractVariable(info *types.Info, curFile cursor.Cursor, start, end token.Pos, all bool) ([]ast.Expr, error) { if start == end { return nil, fmt.Errorf("empty selection") } + file := curFile.Node().(*ast.File) + // TODO(adonovan): simplify, using Cursor. path, exact := astutil.PathEnclosingInterval(file, start, end) if !exact { return nil, fmt.Errorf("selection is not an expression") @@ -571,13 +576,13 @@ type returnVariable struct { } // extractMethod refactors the selected block of code into a new method. -func extractMethod(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - return extractFunctionMethod(fset, start, end, src, file, pkg, info, true) +func extractMethod(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + return extractFunctionMethod(fset, start, end, src, curFile, pkg, info, true) } // extractFunction refactors the selected block of code into a new function. -func extractFunction(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - return extractFunctionMethod(fset, start, end, src, file, pkg, info, false) +func extractFunction(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + return extractFunctionMethod(fset, start, end, src, curFile, pkg, info, false) } // extractFunctionMethod refactors the selected block of code into a new function/method. @@ -588,17 +593,19 @@ func extractFunction(fset *token.FileSet, start, end token.Pos, src []byte, file // and return values of the extracted function/method. Lastly, we construct the call // of the function/method and insert this call as well as the extracted function/method into // their proper locations. -func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info, isMethod bool) (*token.FileSet, *analysis.SuggestedFix, error) { +func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info, isMethod bool) (*token.FileSet, *analysis.SuggestedFix, error) { errorPrefix := "extractFunction" if isMethod { errorPrefix = "extractMethod" } + file := curFile.Node().(*ast.File) + // TODO(adonovan): simplify, using Cursor. tok := fset.File(file.FileStart) if tok == nil { return nil, nil, bug.Errorf("no file for position") } - p, ok, methodOk, err := canExtractFunction(tok, start, end, src, file) + p, ok, methodOk, err := canExtractFunction(tok, start, end, src, curFile) if (!ok && !isMethod) || (!methodOk && isMethod) { return nil, nil, fmt.Errorf("%s: cannot extract %s: %v", errorPrefix, safetoken.StartPosition(fset, start), err) @@ -1086,7 +1093,10 @@ func moveParamToFrontIfFound(params []ast.Expr, paramTypes []*ast.Field, x, sel // their cursors for whitespace. To support this use case, we must manually adjust the // ranges to match the correct AST node. In this particular example, we would adjust // rng.Start forward to the start of 'if' and rng.End backward to after '}'. -func adjustRangeForCommentsAndWhiteSpace(tok *token.File, start, end token.Pos, content []byte, file *ast.File) (token.Pos, token.Pos, error) { +func adjustRangeForCommentsAndWhiteSpace(tok *token.File, start, end token.Pos, content []byte, curFile cursor.Cursor) (token.Pos, token.Pos, error) { + file := curFile.Node().(*ast.File) + // TODO(adonovan): simplify, using Cursor. + // Adjust the end of the range to after leading whitespace and comments. prevStart := token.NoPos startComment := sort.Search(len(file.Comments), func(i int) bool { @@ -1410,12 +1420,14 @@ type fnExtractParams struct { // canExtractFunction reports whether the code in the given range can be // extracted to a function. -func canExtractFunction(tok *token.File, start, end token.Pos, src []byte, file *ast.File) (*fnExtractParams, bool, bool, error) { +func canExtractFunction(tok *token.File, start, end token.Pos, src []byte, curFile cursor.Cursor) (*fnExtractParams, bool, bool, error) { if start == end { return nil, false, false, fmt.Errorf("start and end are equal") } var err error - start, end, err = adjustRangeForCommentsAndWhiteSpace(tok, start, end, src, file) + file := curFile.Node().(*ast.File) + // TODO(adonovan): simplify, using Cursor. + start, end, err = adjustRangeForCommentsAndWhiteSpace(tok, start, end, src, curFile) if err != nil { return nil, false, false, err } diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go index e812c677541..2c14d09c218 100644 --- a/gopls/internal/golang/fix.go +++ b/gopls/internal/golang/fix.go @@ -7,7 +7,6 @@ package golang import ( "context" "fmt" - "go/ast" "go/token" "go/types" @@ -20,6 +19,7 @@ import ( "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/internal/astutil/cursor" "golang.org/x/tools/internal/imports" ) @@ -47,12 +47,12 @@ type fixer func(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf // TODO(adonovan): move fillstruct and undeclaredname into this // package, so we can remove the import restriction and push // the singleFile wrapper down into each singleFileFixer? -type singleFileFixer func(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) +type singleFileFixer func(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) // singleFile adapts a single-file fixer to a Fixer. func singleFile(fixer1 singleFileFixer) fixer { return func(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { - return fixer1(pkg.FileSet(), start, end, pgf.Src, pgf.File, pkg.Types(), pkg.TypesInfo()) + return fixer1(pkg.FileSet(), start, end, pgf.Src, pgf.Cursor, pkg.Types(), pkg.TypesInfo()) } } diff --git a/gopls/internal/golang/invertifcondition.go b/gopls/internal/golang/invertifcondition.go index 0fb7d1e4d0a..b26618ebf93 100644 --- a/gopls/internal/golang/invertifcondition.go +++ b/gopls/internal/golang/invertifcondition.go @@ -14,11 +14,12 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/astutil/cursor" ) // invertIfCondition is a singleFileFixFunc that inverts an if/else statement -func invertIfCondition(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - ifStatement, _, err := canInvertIfCondition(file, start, end) +func invertIfCondition(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + ifStatement, _, err := canInvertIfCondition(curFile, start, end) if err != nil { return nil, nil, err } @@ -241,7 +242,9 @@ func invertAndOr(fset *token.FileSet, expr *ast.BinaryExpr, src []byte) ([]byte, // canInvertIfCondition reports whether we can do invert-if-condition on the // code in the given range. -func canInvertIfCondition(file *ast.File, start, end token.Pos) (*ast.IfStmt, bool, error) { +func canInvertIfCondition(curFile cursor.Cursor, start, end token.Pos) (*ast.IfStmt, bool, error) { + file := curFile.Node().(*ast.File) + // TODO(adonovan): simplify, using Cursor. path, _ := astutil.PathEnclosingInterval(file, start, end) for _, node := range path { stmt, isIfStatement := node.(*ast.IfStmt) diff --git a/gopls/internal/golang/lines.go b/gopls/internal/golang/lines.go index b6a9823957d..f208e33e0c3 100644 --- a/gopls/internal/golang/lines.go +++ b/gopls/internal/golang/lines.go @@ -20,12 +20,13 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/astutil/cursor" ) // canSplitLines checks whether we can split lists of elements inside // an enclosing curly bracket/parens into separate lines. -func canSplitLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) { - itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end) +func canSplitLines(curFile cursor.Cursor, fset *token.FileSet, start, end token.Pos) (string, bool, error) { + itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, curFile, nil, start, end) if itemType == "" { return "", false, nil } @@ -47,8 +48,8 @@ func canSplitLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (s // canJoinLines checks whether we can join lists of elements inside an // enclosing curly bracket/parens into a single line. -func canJoinLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) { - itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end) +func canJoinLines(curFile cursor.Cursor, fset *token.FileSet, start, end token.Pos) (string, bool, error) { + itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, curFile, nil, start, end) if itemType == "" { return "", false, nil } @@ -84,8 +85,8 @@ func canSplitJoinLines(items []ast.Node, comments []*ast.CommentGroup) bool { } // splitLines is a singleFile fixer. -func splitLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - itemType, items, comments, indent, braceOpen, braceClose := findSplitJoinTarget(fset, file, src, start, end) +func splitLines(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + itemType, items, comments, indent, braceOpen, braceClose := findSplitJoinTarget(fset, curFile, src, start, end) if itemType == "" { return nil, nil, nil // no fix available } @@ -94,8 +95,8 @@ func splitLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast } // joinLines is a singleFile fixer. -func joinLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - itemType, items, comments, _, braceOpen, braceClose := findSplitJoinTarget(fset, file, src, start, end) +func joinLines(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + itemType, items, comments, _, braceOpen, braceClose := findSplitJoinTarget(fset, curFile, src, start, end) if itemType == "" { return nil, nil, nil // no fix available } @@ -166,11 +167,14 @@ func processLines(fset *token.FileSet, items []ast.Node, comments []*ast.Comment } // findSplitJoinTarget returns the first curly bracket/parens that encloses the current cursor. -func findSplitJoinTarget(fset *token.FileSet, file *ast.File, src []byte, start, end token.Pos) (itemType string, items []ast.Node, comments []*ast.CommentGroup, indent string, open, close token.Pos) { +func findSplitJoinTarget(fset *token.FileSet, curFile cursor.Cursor, src []byte, start, end token.Pos) (itemType string, items []ast.Node, comments []*ast.CommentGroup, indent string, open, close token.Pos) { isCursorInside := func(nodePos, nodeEnd token.Pos) bool { return nodePos < start && end < nodeEnd } + file := curFile.Node().(*ast.File) + // TODO(adonovan): simplify, using Cursor. + findTarget := func() (targetType string, target ast.Node, open, close token.Pos) { path, _ := astutil.PathEnclosingInterval(file, start, end) for _, node := range path { diff --git a/gopls/internal/golang/stub.go b/gopls/internal/golang/stub.go index a04a82988c5..c85080f8a0c 100644 --- a/gopls/internal/golang/stub.go +++ b/gopls/internal/golang/stub.go @@ -31,8 +31,7 @@ import ( // methods of the concrete type that is assigned to an interface type // at the cursor position. func stubMissingInterfaceMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { - nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end) - si := stubmethods.GetIfaceStubInfo(pkg.FileSet(), pkg.TypesInfo(), nodes, start) + si := stubmethods.GetIfaceStubInfo(pkg.FileSet(), pkg.TypesInfo(), pgf, start, end) if si == nil { return nil, nil, fmt.Errorf("nil interface request") } @@ -43,8 +42,7 @@ func stubMissingInterfaceMethodsFixer(ctx context.Context, snapshot *cache.Snaps // method that the user may want to generate based on CallExpr // at the cursor position. func stubMissingCalledFunctionFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { - nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end) - si := stubmethods.GetCallStubInfo(pkg.FileSet(), pkg.TypesInfo(), nodes, start) + si := stubmethods.GetCallStubInfo(pkg.FileSet(), pkg.TypesInfo(), pgf, start, end) if si == nil { return nil, nil, fmt.Errorf("invalid type request") } diff --git a/gopls/internal/golang/stubmethods/stubcalledfunc.go b/gopls/internal/golang/stubmethods/stubcalledfunc.go index 1b1b6aba7de..b4b59340d83 100644 --- a/gopls/internal/golang/stubmethods/stubcalledfunc.go +++ b/gopls/internal/golang/stubmethods/stubcalledfunc.go @@ -13,6 +13,8 @@ import ( "strings" "unicode" + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/typesinternal" ) @@ -34,7 +36,9 @@ type CallStubInfo struct { // GetCallStubInfo extracts necessary information to generate a method definition from // a CallExpr. -func GetCallStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node, pos token.Pos) *CallStubInfo { +func GetCallStubInfo(fset *token.FileSet, info *types.Info, pgf *parsego.File, start, end token.Pos) *CallStubInfo { + // TODO(adonovan): simplify, using pgf.Cursor. + path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) for i, n := range path { switch n := n.(type) { case *ast.CallExpr: diff --git a/gopls/internal/golang/stubmethods/stubmethods.go b/gopls/internal/golang/stubmethods/stubmethods.go index f380f5b984d..a060993b1ab 100644 --- a/gopls/internal/golang/stubmethods/stubmethods.go +++ b/gopls/internal/golang/stubmethods/stubmethods.go @@ -15,8 +15,10 @@ import ( "go/types" "strings" + "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/typesutil" ) @@ -49,7 +51,12 @@ type IfaceStubInfo struct { // function call. This is essentially what the refactor/satisfy does, // more generally. Refactor to share logic, after auditing 'satisfy' // for safety on ill-typed code. -func GetIfaceStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node, pos token.Pos) *IfaceStubInfo { +func GetIfaceStubInfo(fset *token.FileSet, info *types.Info, pgf *parsego.File, pos, end token.Pos) *IfaceStubInfo { + // TODO(adonovan): simplify, using Cursor: + // curErr, _ := pgf.Cursor.FindPos(pos, end) + // for cur := range curErr.Ancestors() { + // switch n := cur.Node().(type) {... + path, _ := astutil.PathEnclosingInterval(pgf.File, pos, end) for _, n := range path { switch n := n.(type) { case *ast.ValueSpec: diff --git a/gopls/internal/golang/undeclared.go b/gopls/internal/golang/undeclared.go index 0615386e9bf..f9331348f47 100644 --- a/gopls/internal/golang/undeclared.go +++ b/gopls/internal/golang/undeclared.go @@ -17,6 +17,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/util/typesutil" + "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/typesinternal" ) @@ -69,8 +70,10 @@ func undeclaredFixTitle(path []ast.Node, errMsg string) string { } // createUndeclared generates a suggested declaration for an undeclared variable or function. -func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { pos := start // don't use the end + file := curFile.Node().(*ast.File) + // TODO(adonovan): simplify, using Cursor. path, _ := astutil.PathEnclosingInterval(file, pos, pos) if len(path) < 2 { return nil, nil, fmt.Errorf("no expression found") diff --git a/gopls/internal/util/typesutil/typesutil.go b/gopls/internal/util/typesutil/typesutil.go index 79042a24901..4b5c5e7fd4f 100644 --- a/gopls/internal/util/typesutil/typesutil.go +++ b/gopls/internal/util/typesutil/typesutil.go @@ -42,6 +42,8 @@ func FormatTypeParams(tparams *types.TypeParamList) string { // the hole that must be filled by EXPR has type (string, int). // // It returns nil on failure. +// +// TODO(adonovan): simplify using Cursor. func TypesFromContext(info *types.Info, path []ast.Node, pos token.Pos) []types.Type { anyType := types.Universe.Lookup("any").Type() var typs []types.Type From cd01e86527e7f9c4cb689b66c5313bf739674c09 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 19 Feb 2025 12:22:56 -0500 Subject: [PATCH 40/99] gopls/internal/golang: make singleFileFixer like fixer In the past the singleFileFixer has two roles: to simplify the signature for fixers of a single file, and to allow them to be expressed only in terms of go/{ast,types} data types, allowing fixers to be more loosely coupled to gopls. But that latter role seems unimportant now, so this CL simplifies the two functions to make them more alike. Change-Id: I42ee9fd275e344fafee0b27b9861f8f599f89e3e Reviewed-on: https://go-review.googlesource.com/c/tools/+/650641 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- .../analysis/fillstruct/fillstruct.go | 21 ++++----- gopls/internal/golang/extract.go | 43 ++++++++++++------- gopls/internal/golang/fix.go | 18 +++----- gopls/internal/golang/invertifcondition.go | 12 ++++-- gopls/internal/golang/lines.go | 17 +++++--- gopls/internal/golang/undeclared.go | 19 +++++--- 6 files changed, 76 insertions(+), 54 deletions(-) diff --git a/gopls/internal/analysis/fillstruct/fillstruct.go b/gopls/internal/analysis/fillstruct/fillstruct.go index 641b98e6fa7..62f7d77f58e 100644 --- a/gopls/internal/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/analysis/fillstruct/fillstruct.go @@ -25,10 +25,11 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/fuzzy" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" ) @@ -130,15 +131,15 @@ const FixCategory = "fillstruct" // recognized by gopls ApplyFix // SuggestedFix computes the suggested fix for the kinds of // diagnostics produced by the Analyzer above. -func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - if info == nil { - return nil, nil, fmt.Errorf("nil types.Info") - } - - pos := start // don't use the end - +func SuggestedFix(cpkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + var ( + fset = cpkg.FileSet() + pkg = cpkg.Types() + info = cpkg.TypesInfo() + pos = start // don't use end + ) // TODO(adonovan): simplify, using Cursor. - file := curFile.Node().(*ast.File) + file := pgf.Cursor.Node().(*ast.File) path, _ := astutil.PathEnclosingInterval(file, pos, pos) if len(path) == 0 { return nil, nil, fmt.Errorf("no enclosing ast.Node") @@ -235,7 +236,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, cur } // Find the line on which the composite literal is declared. - split := bytes.Split(content, []byte("\n")) + split := bytes.Split(pgf.Src, []byte("\n")) lineNumber := safetoken.StartPosition(fset, expr.Lbrace).Line firstLine := split[lineNumber-1] // lines are 1-indexed diff --git a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go index 3d2b880db2d..f73e772e676 100644 --- a/gopls/internal/golang/extract.go +++ b/gopls/internal/golang/extract.go @@ -20,6 +20,8 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" @@ -29,13 +31,13 @@ import ( ) // extractVariable implements the refactor.extract.{variable,constant} CodeAction command. -func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, _ *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - return extractExprs(fset, start, end, src, curFile, info, false) +func extractVariable(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + return extractExprs(pkg, pgf, start, end, false) } // extractVariableAll implements the refactor.extract.{variable,constant}-all CodeAction command. -func extractVariableAll(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, _ *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - return extractExprs(fset, start, end, src, curFile, info, true) +func extractVariableAll(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + return extractExprs(pkg, pgf, start, end, true) } // extractExprs replaces occurrence(s) of a specified expression within the same function @@ -44,11 +46,15 @@ func extractVariableAll(fset *token.FileSet, start, end token.Pos, src []byte, c // // The new variable/constant is declared as close as possible to the first found expression // within the deepest common scope accessible to all candidate occurrences. -func extractExprs(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, info *types.Info, all bool) (*token.FileSet, *analysis.SuggestedFix, error) { - file := curFile.Node().(*ast.File) +func extractExprs(pkg *cache.Package, pgf *parsego.File, start, end token.Pos, all bool) (*token.FileSet, *analysis.SuggestedFix, error) { + var ( + fset = pkg.FileSet() + info = pkg.TypesInfo() + file = pgf.File + ) // TODO(adonovan): simplify, using Cursor. tokFile := fset.File(file.FileStart) - exprs, err := canExtractVariable(info, curFile, start, end, all) + exprs, err := canExtractVariable(info, pgf.Cursor, start, end, all) if err != nil { return nil, nil, fmt.Errorf("cannot extract: %v", err) } @@ -157,7 +163,7 @@ Outer: return nil, nil, fmt.Errorf("cannot find location to insert extraction: %v", err) } // Within function: compute appropriate statement indentation. - indent, err := calculateIndentation(src, tokFile, before) + indent, err := calculateIndentation(pgf.Src, tokFile, before) if err != nil { return nil, nil, err } @@ -576,13 +582,13 @@ type returnVariable struct { } // extractMethod refactors the selected block of code into a new method. -func extractMethod(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - return extractFunctionMethod(fset, start, end, src, curFile, pkg, info, true) +func extractMethod(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + return extractFunctionMethod(pkg, pgf, start, end, true) } // extractFunction refactors the selected block of code into a new function. -func extractFunction(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - return extractFunctionMethod(fset, start, end, src, curFile, pkg, info, false) +func extractFunction(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + return extractFunctionMethod(pkg, pgf, start, end, false) } // extractFunctionMethod refactors the selected block of code into a new function/method. @@ -593,19 +599,26 @@ func extractFunction(fset *token.FileSet, start, end token.Pos, src []byte, curF // and return values of the extracted function/method. Lastly, we construct the call // of the function/method and insert this call as well as the extracted function/method into // their proper locations. -func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info, isMethod bool) (*token.FileSet, *analysis.SuggestedFix, error) { +func extractFunctionMethod(cpkg *cache.Package, pgf *parsego.File, start, end token.Pos, isMethod bool) (*token.FileSet, *analysis.SuggestedFix, error) { + var ( + fset = cpkg.FileSet() + pkg = cpkg.Types() + info = cpkg.TypesInfo() + src = pgf.Src + ) + errorPrefix := "extractFunction" if isMethod { errorPrefix = "extractMethod" } - file := curFile.Node().(*ast.File) + file := pgf.Cursor.Node().(*ast.File) // TODO(adonovan): simplify, using Cursor. tok := fset.File(file.FileStart) if tok == nil { return nil, nil, bug.Errorf("no file for position") } - p, ok, methodOk, err := canExtractFunction(tok, start, end, src, curFile) + p, ok, methodOk, err := canExtractFunction(tok, start, end, src, pgf.Cursor) if (!ok && !isMethod) || (!methodOk && isMethod) { return nil, nil, fmt.Errorf("%s: cannot extract %s: %v", errorPrefix, safetoken.StartPosition(fset, start), err) diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go index 2c14d09c218..dbd83ef071f 100644 --- a/gopls/internal/golang/fix.go +++ b/gopls/internal/golang/fix.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "go/token" - "go/types" "golang.org/x/tools/go/analysis" "golang.org/x/tools/gopls/internal/analysis/embeddirective" @@ -19,7 +18,6 @@ import ( "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/internal/astutil/cursor" "golang.org/x/tools/internal/imports" ) @@ -41,18 +39,14 @@ import ( // A fixer may return (nil, nil) if no fix is available. type fixer func(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) -// A singleFileFixer is a Fixer that inspects only a single file, -// and does not depend on data types from the cache package. -// -// TODO(adonovan): move fillstruct and undeclaredname into this -// package, so we can remove the import restriction and push -// the singleFile wrapper down into each singleFileFixer? -type singleFileFixer func(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) +// A singleFileFixer is a [fixer] that inspects only a single file. +type singleFileFixer func(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) -// singleFile adapts a single-file fixer to a Fixer. +// singleFile adapts a [singleFileFixer] to a [fixer] +// by discarding the snapshot and the context it needs. func singleFile(fixer1 singleFileFixer) fixer { - return func(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { - return fixer1(pkg.FileSet(), start, end, pgf.Src, pgf.Cursor, pkg.Types(), pkg.TypesInfo()) + return func(_ context.Context, _ *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + return fixer1(pkg, pgf, start, end) } } diff --git a/gopls/internal/golang/invertifcondition.go b/gopls/internal/golang/invertifcondition.go index b26618ebf93..012278df79e 100644 --- a/gopls/internal/golang/invertifcondition.go +++ b/gopls/internal/golang/invertifcondition.go @@ -8,18 +8,24 @@ import ( "fmt" "go/ast" "go/token" - "go/types" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/astutil/cursor" ) // invertIfCondition is a singleFileFixFunc that inverts an if/else statement -func invertIfCondition(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - ifStatement, _, err := canInvertIfCondition(curFile, start, end) +func invertIfCondition(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + var ( + fset = pkg.FileSet() + src = pgf.Src + ) + + ifStatement, _, err := canInvertIfCondition(pgf.Cursor, start, end) if err != nil { return nil, nil, err } diff --git a/gopls/internal/golang/lines.go b/gopls/internal/golang/lines.go index f208e33e0c3..cb161671726 100644 --- a/gopls/internal/golang/lines.go +++ b/gopls/internal/golang/lines.go @@ -12,13 +12,14 @@ import ( "bytes" "go/ast" "go/token" - "go/types" "slices" "sort" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/astutil/cursor" ) @@ -85,23 +86,25 @@ func canSplitJoinLines(items []ast.Node, comments []*ast.CommentGroup) bool { } // splitLines is a singleFile fixer. -func splitLines(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - itemType, items, comments, indent, braceOpen, braceClose := findSplitJoinTarget(fset, curFile, src, start, end) +func splitLines(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + fset := pkg.FileSet() + itemType, items, comments, indent, braceOpen, braceClose := findSplitJoinTarget(fset, pgf.Cursor, pgf.Src, start, end) if itemType == "" { return nil, nil, nil // no fix available } - return fset, processLines(fset, items, comments, src, braceOpen, braceClose, ",\n", "\n", ",\n"+indent, indent+"\t"), nil + return fset, processLines(fset, items, comments, pgf.Src, braceOpen, braceClose, ",\n", "\n", ",\n"+indent, indent+"\t"), nil } // joinLines is a singleFile fixer. -func joinLines(fset *token.FileSet, start, end token.Pos, src []byte, curFile cursor.Cursor, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - itemType, items, comments, _, braceOpen, braceClose := findSplitJoinTarget(fset, curFile, src, start, end) +func joinLines(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + fset := pkg.FileSet() + itemType, items, comments, _, braceOpen, braceClose := findSplitJoinTarget(fset, pgf.Cursor, pgf.Src, start, end) if itemType == "" { return nil, nil, nil // no fix available } - return fset, processLines(fset, items, comments, src, braceOpen, braceClose, ", ", "", "", ""), nil + return fset, processLines(fset, items, comments, pgf.Src, braceOpen, braceClose, ", ", "", "", ""), nil } // processLines is the common operation for both split and join lines because this split/join operation is diff --git a/gopls/internal/golang/undeclared.go b/gopls/internal/golang/undeclared.go index f9331348f47..9df8e2bfd2e 100644 --- a/gopls/internal/golang/undeclared.go +++ b/gopls/internal/golang/undeclared.go @@ -16,8 +16,9 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/util/typesutil" - "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/typesinternal" ) @@ -70,9 +71,13 @@ func undeclaredFixTitle(path []ast.Node, errMsg string) string { } // createUndeclared generates a suggested declaration for an undeclared variable or function. -func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, curFile cursor.Cursor, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { - pos := start // don't use the end - file := curFile.Node().(*ast.File) +func createUndeclared(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + var ( + fset = pkg.FileSet() + info = pkg.TypesInfo() + file = pgf.File + pos = start // don't use end + ) // TODO(adonovan): simplify, using Cursor. path, _ := astutil.PathEnclosingInterval(file, pos, pos) if len(path) < 2 { @@ -86,7 +91,7 @@ func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, // Check for a possible call expression, in which case we should add a // new function declaration. if isCallPosition(path) { - return newFunctionDeclaration(path, file, pkg, info, fset) + return newFunctionDeclaration(path, file, pkg.Types(), info, fset) } var ( firstRef *ast.Ident // We should insert the new declaration before the first occurrence of the undefined ident. @@ -132,7 +137,7 @@ func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, if err != nil { return nil, nil, fmt.Errorf("could not locate insertion point: %v", err) } - indent, err := calculateIndentation(content, fset.File(file.FileStart), insertBeforeStmt) + indent, err := calculateIndentation(pgf.Src, fset.File(file.FileStart), insertBeforeStmt) if err != nil { return nil, nil, err } @@ -141,7 +146,7 @@ func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, // Default to 0. typs = []types.Type{types.Typ[types.Int]} } - expr, _ := typesinternal.ZeroExpr(typs[0], typesinternal.FileQualifier(file, pkg)) + expr, _ := typesinternal.ZeroExpr(typs[0], typesinternal.FileQualifier(file, pkg.Types())) assignStmt := &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent(ident.Name)}, Tok: token.DEFINE, From 0b693ed05c20dd39478c8afb542bb4473dde7ba7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 19 Feb 2025 14:22:09 -0500 Subject: [PATCH 41/99] internal/astutil/cursor: FindPos: don't assume Files are in Pos order + test Change-Id: I75c4c3b7789feda874da7644bda8ba93f87f5efb Reviewed-on: https://go-review.googlesource.com/c/tools/+/650645 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- internal/astutil/cursor/cursor.go | 18 ++++++++++++------ internal/astutil/cursor/cursor_test.go | 26 +++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/internal/astutil/cursor/cursor.go b/internal/astutil/cursor/cursor.go index 5ed177c9f3d..83a47e09058 100644 --- a/internal/astutil/cursor/cursor.go +++ b/internal/astutil/cursor/cursor.go @@ -428,15 +428,21 @@ func (c Cursor) FindPos(start, end token.Pos) (Cursor, bool) { ev := events[i] if ev.index > i { // push? n := ev.node - var nodeStart, nodeEnd token.Pos + var nodeEnd token.Pos if file, ok := n.(*ast.File); ok { - nodeStart, nodeEnd = file.FileStart, file.FileEnd + nodeEnd = file.FileEnd + // Note: files may be out of Pos order. + if file.FileStart > start { + i = ev.index // disjoint, after; skip to next file + continue + } } else { - nodeStart, nodeEnd = n.Pos(), n.End() - } - if nodeStart > start { - break // disjoint, after; stop + nodeEnd = n.End() + if n.Pos() > start { + break // disjoint, after; stop + } } + // Inv: node.{Pos,FileStart} <= start if end <= nodeEnd { // node fully contains target range best = i diff --git a/internal/astutil/cursor/cursor_test.go b/internal/astutil/cursor/cursor_test.go index 01f791f2833..67e91544c4d 100644 --- a/internal/astutil/cursor/cursor_test.go +++ b/internal/astutil/cursor/cursor_test.go @@ -14,6 +14,7 @@ import ( "go/token" "iter" "log" + "math/rand" "path/filepath" "reflect" "slices" @@ -332,8 +333,31 @@ func TestCursor_FindNode(t *testing.T) { } } } +} + +// TestCursor_FindPos_order ensures that FindPos does not assume files are in Pos order. +func TestCursor_FindPos_order(t *testing.T) { + // Pick an arbitrary decl. + target := netFiles[7].Decls[0] + + // Find the target decl by its position. + cur, ok := cursor.Root(netInspect).FindPos(target.Pos(), target.End()) + if !ok || cur.Node() != target { + t.Fatalf("unshuffled: FindPos(%T) = (%v, %t)", target, cur, ok) + } - // TODO(adonovan): FindPos needs a test (not just a benchmark). + // Shuffle the files out of Pos order. + files := slices.Clone(netFiles) + rand.Shuffle(len(files), func(i, j int) { + files[i], files[j] = files[j], files[i] + }) + + // Find it again. + inspect := inspector.New(files) + cur, ok = cursor.Root(inspect).FindPos(target.Pos(), target.End()) + if !ok || cur.Node() != target { + t.Fatalf("shuffled: FindPos(%T) = (%v, %t)", target, cur, ok) + } } func TestCursor_Edge(t *testing.T) { From 107c5b255f0e491b4e9f5ce6d3be554e07a38caa Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 19 Feb 2025 15:28:28 -0500 Subject: [PATCH 42/99] gopls/internal/analysis/modernize: disable unsound maps.Clone fix The fix is sound only if the operand is provably non-nil. My word, these simple cases are subtle. Are we wrong to want to expand access to the framework? Fixes golang/go#71844 Change-Id: I5cf414cae41bf9b6445b7a4a7175dbb9c292e4b8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650756 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- gopls/internal/analysis/modernize/maps.go | 14 ++++++- .../testdata/src/mapsloop/mapsloop.go | 30 ++++++++++----- .../testdata/src/mapsloop/mapsloop.go.golden | 37 ++++++++++++++----- .../testdata/src/mapsloop/mapsloop_dot.go | 6 ++- .../src/mapsloop/mapsloop_dot.go.golden | 8 ++-- 5 files changed, 69 insertions(+), 26 deletions(-) diff --git a/gopls/internal/analysis/modernize/maps.go b/gopls/internal/analysis/modernize/maps.go index 91de659d107..dad329477cd 100644 --- a/gopls/internal/analysis/modernize/maps.go +++ b/gopls/internal/analysis/modernize/maps.go @@ -32,7 +32,7 @@ import ( // // maps.Copy(m, x) (x is map) // maps.Insert(m, x) (x is iter.Seq2) -// m = maps.Clone(x) (x is map, m is a new map) +// m = maps.Clone(x) (x is a non-nil map, m is a new map) // m = maps.Collect(x) (x is iter.Seq2, m is a new map) // // A map is newly created if the preceding statement has one of these @@ -77,6 +77,8 @@ func mapsloop(pass *analysis.Pass) { // Is the preceding statement of the form // m = make(M) or M{} // and can we replace its RHS with slices.{Clone,Collect}? + // + // Beware: if x may be nil, we cannot use Clone as it preserves nilness. var mrhs ast.Expr // make(M) or M{}, or nil if curPrev, ok := curRange.PrevSibling(); ok { if assign, ok := curPrev.Node().(*ast.AssignStmt); ok && @@ -122,6 +124,16 @@ func mapsloop(pass *analysis.Pass) { mrhs = rhs } } + + // Temporarily disable the transformation to the + // (nil-preserving) maps.Clone until we can prove + // that x is non-nil. This is rarely possible, + // and may require control flow analysis + // (e.g. a dominating "if len(x)" check). + // See #71844. + if xmap { + mrhs = nil + } } } } diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go index e4e6963dbae..68ff9154ffd 100644 --- a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go +++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go @@ -27,30 +27,34 @@ func useCopyGeneric[K comparable, V any, M ~map[K]V](dst, src M) { } } -func useClone(src map[int]string) { - // Replace make(...) by maps.Clone. +func useCopyNotClone(src map[int]string) { + // Clone is tempting but wrong when src may be nil; see #71844. + + // Replace make(...) by maps.Copy. dst := make(map[int]string, len(src)) for key, value := range src { - dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" } dst = map[int]string{} for key, value := range src { - dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" } println(dst) } -func useCloneParen(src map[int]string) { +func useCopyParen(src map[int]string) { + // Clone is tempting but wrong when src may be nil; see #71844. + // Replace (make)(...) by maps.Clone. dst := (make)(map[int]string, len(src)) for key, value := range src { - dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" } dst = (map[int]string{}) for key, value := range src { - dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" } println(dst) } @@ -74,32 +78,38 @@ func useCopy_typesDiffer2(src map[int]string) { } func useClone_typesDiffer3(src map[int]string) { + // Clone is tempting but wrong when src may be nil; see #71844. + // Replace loop and make(...) as maps.Clone(src) returns map[int]string // which is assignable to M. var dst M dst = make(M, len(src)) for key, value := range src { - dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" } println(dst) } func useClone_typesDiffer4(src map[int]string) { + // Clone is tempting but wrong when src may be nil; see #71844. + // Replace loop and make(...) as maps.Clone(src) returns map[int]string // which is assignable to M. var dst M dst = make(M, len(src)) for key, value := range src { - dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" } println(dst) } func useClone_generic[Map ~map[K]V, K comparable, V any](src Map) { + // Clone is tempting but wrong when src may be nil; see #71844. + // Replace loop and make(...) by maps.Clone dst := make(Map, len(src)) for key, value := range src { - dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" } println(dst) } diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden index 70b9a28ed5b..be189673d9a 100644 --- a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop.go.golden @@ -23,19 +23,27 @@ func useCopyGeneric[K comparable, V any, M ~map[K]V](dst, src M) { maps.Copy(dst, src) } -func useClone(src map[int]string) { - // Replace make(...) by maps.Clone. - dst := maps.Clone(src) +func useCopyNotClone(src map[int]string) { + // Clone is tempting but wrong when src may be nil; see #71844. - dst = maps.Clone(src) + // Replace make(...) by maps.Copy. + dst := make(map[int]string, len(src)) + maps.Copy(dst, src) + + dst = map[int]string{} + maps.Copy(dst, src) println(dst) } -func useCloneParen(src map[int]string) { +func useCopyParen(src map[int]string) { + // Clone is tempting but wrong when src may be nil; see #71844. + // Replace (make)(...) by maps.Clone. - dst := maps.Clone(src) + dst := (make)(map[int]string, len(src)) + maps.Copy(dst, src) - dst = maps.Clone(src) + dst = (map[int]string{}) + maps.Copy(dst, src) println(dst) } @@ -54,24 +62,33 @@ func useCopy_typesDiffer2(src map[int]string) { } func useClone_typesDiffer3(src map[int]string) { + // Clone is tempting but wrong when src may be nil; see #71844. + // Replace loop and make(...) as maps.Clone(src) returns map[int]string // which is assignable to M. var dst M - dst = maps.Clone(src) + dst = make(M, len(src)) + maps.Copy(dst, src) println(dst) } func useClone_typesDiffer4(src map[int]string) { + // Clone is tempting but wrong when src may be nil; see #71844. + // Replace loop and make(...) as maps.Clone(src) returns map[int]string // which is assignable to M. var dst M - dst = maps.Clone(src) + dst = make(M, len(src)) + maps.Copy(dst, src) println(dst) } func useClone_generic[Map ~map[K]V, K comparable, V any](src Map) { + // Clone is tempting but wrong when src may be nil; see #71844. + // Replace loop and make(...) by maps.Clone - dst := maps.Clone(src) + dst := make(Map, len(src)) + maps.Copy(dst, src) println(dst) } diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go index c33d43e23ad..ae28f11afda 100644 --- a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go +++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go @@ -14,10 +14,12 @@ func useCopyDot(dst, src map[int]string) { } func useCloneDot(src map[int]string) { - // Replace make(...) by maps.Clone. + // Clone is tempting but wrong when src may be nil; see #71844. + + // Replace make(...) by maps.Copy. dst := make(map[int]string, len(src)) for key, value := range src { - dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Clone" + dst[key] = value // want "Replace m\\[k\\]=v loop with maps.Copy" } println(dst) } diff --git a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go.golden b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go.golden index d6a30537645..e992314cf56 100644 --- a/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/mapsloop/mapsloop_dot.go.golden @@ -12,8 +12,10 @@ func useCopyDot(dst, src map[int]string) { } func useCloneDot(src map[int]string) { - // Replace make(...) by maps.Clone. - dst := Clone(src) + // Clone is tempting but wrong when src may be nil; see #71844. + + // Replace make(...) by maps.Copy. + dst := make(map[int]string, len(src)) + Copy(dst, src) println(dst) } - From 99337ebe7b90918701a41932abf121600b972e34 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 19 Feb 2025 17:40:54 -0500 Subject: [PATCH 43/99] x/tools: modernize interface{} -> any Produced with a patched version of modernize containing only the efaceany pass: $ go run ./gopls/internal/analysis/modernize/cmd/modernize/main.go -test -fix ./... ./gopls/... This is very safe and forms the bulk of the modernize diff, isolating the other changes for ease of review. Change-Id: Iec9352bac5a639a5c03368427531c7842c6e9ad0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650759 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- blog/blog.go | 2 +- container/intsets/sparse.go | 2 +- go/analysis/analysis.go | 8 +- go/analysis/analysistest/analysistest.go | 2 +- go/analysis/analysistest/analysistest_test.go | 2 +- go/analysis/internal/analysisflags/flags.go | 10 +- go/analysis/internal/checker/checker_test.go | 6 +- go/analysis/internal/checker/start_test.go | 2 +- go/analysis/internal/internal.go | 2 +- .../internal/versiontest/version_test.go | 2 +- go/analysis/multichecker/multichecker_test.go | 2 +- go/analysis/passes/appends/appends.go | 2 +- go/analysis/passes/asmdecl/asmdecl.go | 6 +- go/analysis/passes/buildssa/buildssa.go | 2 +- go/analysis/passes/buildtag/buildtag.go | 2 +- go/analysis/passes/cgocall/cgocall.go | 2 +- go/analysis/passes/composite/composite.go | 2 +- go/analysis/passes/copylock/copylock.go | 2 +- go/analysis/passes/ctrlflow/ctrlflow.go | 2 +- go/analysis/passes/directive/directive.go | 2 +- .../passes/fieldalignment/fieldalignment.go | 2 +- go/analysis/passes/findcall/findcall.go | 2 +- .../passes/framepointer/framepointer.go | 2 +- go/analysis/passes/ifaceassert/ifaceassert.go | 2 +- go/analysis/passes/inspect/inspect.go | 2 +- go/analysis/passes/loopclosure/loopclosure.go | 2 +- go/analysis/passes/lostcancel/lostcancel.go | 2 +- go/analysis/passes/nilfunc/nilfunc.go | 2 +- go/analysis/passes/nilness/nilness.go | 4 +- go/analysis/passes/pkgfact/pkgfact.go | 2 +- .../reflectvaluecompare.go | 2 +- go/analysis/passes/shadow/shadow.go | 2 +- go/analysis/passes/shift/shift.go | 2 +- go/analysis/passes/stdmethods/stdmethods.go | 2 +- go/analysis/passes/stringintconv/string.go | 2 +- go/analysis/passes/structtag/structtag.go | 2 +- .../testinggoroutine/testinggoroutine.go | 2 +- go/analysis/passes/tests/tests.go | 2 +- go/analysis/passes/unreachable/unreachable.go | 2 +- go/analysis/passes/unsafeptr/unsafeptr.go | 2 +- .../passes/unusedresult/unusedresult.go | 2 +- .../passes/usesgenerics/usesgenerics.go | 2 +- go/analysis/unitchecker/unitchecker.go | 4 +- go/analysis/unitchecker/unitchecker_test.go | 2 +- go/analysis/validate_test.go | 4 +- go/buildutil/fakecontext.go | 4 +- go/buildutil/tags.go | 2 +- go/callgraph/rta/rta.go | 4 +- go/callgraph/rta/rta_test.go | 2 +- go/callgraph/vta/internal/trie/builder.go | 20 +- go/callgraph/vta/internal/trie/op_test.go | 48 ++-- go/callgraph/vta/internal/trie/trie.go | 22 +- go/callgraph/vta/internal/trie/trie_test.go | 214 +++++++++--------- go/callgraph/vta/propagation.go | 2 +- go/callgraph/vta/vta_test.go | 2 +- go/expect/expect.go | 8 +- go/expect/expect_test.go | 6 +- go/expect/extract.go | 12 +- go/internal/cgo/cgo.go | 2 +- go/internal/gccgoimporter/parser.go | 32 +-- go/loader/loader.go | 2 +- go/packages/gopackages/main.go | 2 +- go/packages/overlay_test.go | 36 +-- go/packages/packages.go | 10 +- go/packages/packages_test.go | 104 ++++----- go/packages/packagestest/expect.go | 36 +-- go/packages/packagestest/expect_test.go | 2 +- go/packages/packagestest/export.go | 10 +- go/packages/packagestest/export_test.go | 26 +-- go/ssa/const_test.go | 6 +- go/ssa/interp/interp.go | 2 +- go/ssa/interp/map.go | 2 +- go/ssa/interp/value.go | 10 +- go/ssa/mode.go | 2 +- go/ssa/print.go | 2 +- go/ssa/sanity.go | 6 +- go/ssa/ssautil/load_test.go | 2 +- go/ssa/util.go | 2 +- .../analysis/deprecated/deprecated.go | 2 +- .../analysis/embeddirective/embeddirective.go | 2 +- .../analysis/fillreturns/fillreturns.go | 2 +- .../internal/analysis/nonewvars/nonewvars.go | 2 +- .../analysis/noresultvalues/noresultvalues.go | 2 +- .../simplifycompositelit.go | 2 +- .../analysis/simplifyrange/simplifyrange.go | 2 +- .../analysis/simplifyslice/simplifyslice.go | 2 +- gopls/internal/analysis/yield/yield.go | 2 +- gopls/internal/cache/analysis.go | 6 +- gopls/internal/cache/load.go | 2 +- gopls/internal/cache/mod.go | 10 +- gopls/internal/cache/mod_tidy.go | 2 +- gopls/internal/cache/mod_vuln.go | 2 +- gopls/internal/cache/parse_cache.go | 6 +- gopls/internal/cmd/cmd.go | 12 +- gopls/internal/cmd/integration_test.go | 4 +- gopls/internal/cmd/stats.go | 2 +- gopls/internal/cmd/symbols.go | 4 +- gopls/internal/debug/log/log.go | 2 +- gopls/internal/debug/rpc.go | 2 +- gopls/internal/debug/serve.go | 22 +- gopls/internal/debug/template_test.go | 2 +- gopls/internal/debug/trace.go | 2 +- gopls/internal/golang/rename_check.go | 2 +- gopls/internal/lsprpc/binder_test.go | 2 +- .../lsprpc/commandinterceptor_test.go | 10 +- gopls/internal/lsprpc/export_test.go | 4 +- gopls/internal/lsprpc/goenv.go | 2 +- gopls/internal/lsprpc/goenv_test.go | 18 +- gopls/internal/lsprpc/lsprpc.go | 12 +- gopls/internal/lsprpc/lsprpc_test.go | 6 +- gopls/internal/lsprpc/middleware_test.go | 2 +- gopls/internal/server/command.go | 2 +- gopls/internal/server/general.go | 4 +- gopls/internal/server/unimplemented.go | 2 +- gopls/internal/template/parse.go | 2 +- .../test/integration/bench/completion_test.go | 2 +- .../test/integration/bench/didchange_test.go | 2 +- gopls/internal/test/integration/env.go | 2 +- gopls/internal/test/integration/env_test.go | 2 +- .../internal/test/integration/expectation.go | 6 +- .../internal/test/integration/fake/client.go | 6 +- .../test/integration/fake/glob/glob.go | 2 +- gopls/internal/test/integration/options.go | 6 +- gopls/internal/util/bug/bug.go | 4 +- gopls/internal/vulncheck/vulntest/report.go | 2 +- internal/event/export/id.go | 2 +- internal/event/export/metric/exporter.go | 4 +- internal/event/export/ocagent/ocagent.go | 4 +- .../event/export/prometheus/prometheus.go | 2 +- internal/event/keys/keys.go | 6 +- internal/event/label/label.go | 6 +- internal/expect/expect.go | 2 +- internal/expect/expect_test.go | 2 +- internal/expect/extract.go | 4 +- internal/facts/facts.go | 2 +- internal/gcimporter/bimport.go | 2 +- internal/gcimporter/iexport.go | 6 +- internal/gcimporter/iimport.go | 2 +- internal/gopathwalk/walk.go | 4 +- internal/imports/fix_test.go | 2 +- internal/jsonrpc2/conn.go | 10 +- internal/jsonrpc2/handler.go | 8 +- internal/jsonrpc2/jsonrpc2_test.go | 12 +- internal/jsonrpc2/messages.go | 8 +- internal/jsonrpc2_v2/conn.go | 14 +- internal/jsonrpc2_v2/jsonrpc2.go | 16 +- internal/jsonrpc2_v2/jsonrpc2_test.go | 28 +-- internal/jsonrpc2_v2/messages.go | 12 +- internal/jsonrpc2_v2/serve_test.go | 4 +- internal/jsonrpc2_v2/wire.go | 2 +- internal/jsonrpc2_v2/wire_test.go | 8 +- internal/memoize/memoize.go | 20 +- internal/memoize/memoize_test.go | 20 +- internal/packagesinternal/packages.go | 2 +- internal/packagestest/expect.go | 36 +-- internal/packagestest/expect_test.go | 2 +- internal/packagestest/export.go | 10 +- internal/packagestest/export_test.go | 26 +-- internal/tool/tool.go | 2 +- internal/typeparams/normalize.go | 2 +- internal/xcontext/xcontext.go | 8 +- 161 files changed, 630 insertions(+), 630 deletions(-) diff --git a/blog/blog.go b/blog/blog.go index 947c60e95a2..901b53f440e 100644 --- a/blog/blog.go +++ b/blog/blog.go @@ -420,7 +420,7 @@ type rootData struct { BasePath string GodocURL string AnalyticsHTML template.HTML - Data interface{} + Data any } // ServeHTTP serves the front, index, and article pages diff --git a/container/intsets/sparse.go b/container/intsets/sparse.go index c56aacc28bb..b9b4c91ed21 100644 --- a/container/intsets/sparse.go +++ b/container/intsets/sparse.go @@ -267,7 +267,7 @@ func (s *Sparse) init() { // loop. Fail fast before this occurs. // We don't want to call panic here because it prevents the // inlining of this function. - _ = (interface{}(nil)).(to_copy_a_sparse_you_must_call_its_Copy_method) + _ = (any(nil)).(to_copy_a_sparse_you_must_call_its_Copy_method) } } diff --git a/go/analysis/analysis.go b/go/analysis/analysis.go index 3a73084a53c..a7df4d1fe4e 100644 --- a/go/analysis/analysis.go +++ b/go/analysis/analysis.go @@ -45,7 +45,7 @@ type Analyzer struct { // To pass analysis results between packages (and thus // potentially between address spaces), use Facts, which are // serializable. - Run func(*Pass) (interface{}, error) + Run func(*Pass) (any, error) // RunDespiteErrors allows the driver to invoke // the Run method of this analyzer even on a @@ -112,7 +112,7 @@ type Pass struct { // The map keys are the elements of Analysis.Required, // and the type of each corresponding value is the required // analysis's ResultType. - ResultOf map[*Analyzer]interface{} + ResultOf map[*Analyzer]any // ReadFile returns the contents of the named file. // @@ -186,7 +186,7 @@ type ObjectFact struct { // Reportf is a helper function that reports a Diagnostic using the // specified position and formatted error message. -func (pass *Pass) Reportf(pos token.Pos, format string, args ...interface{}) { +func (pass *Pass) Reportf(pos token.Pos, format string, args ...any) { msg := fmt.Sprintf(format, args...) pass.Report(Diagnostic{Pos: pos, Message: msg}) } @@ -201,7 +201,7 @@ type Range interface { // ReportRangef is a helper function that reports a Diagnostic using the // range provided. ast.Node values can be passed in as the range because // they satisfy the Range interface. -func (pass *Pass) ReportRangef(rng Range, format string, args ...interface{}) { +func (pass *Pass) ReportRangef(rng Range, format string, args ...any) { msg := fmt.Sprintf(format, args...) pass.Report(Diagnostic{Pos: rng.Pos(), End: rng.End(), Message: msg}) } diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index 08981776478..0b5cfe70bfe 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -76,7 +76,7 @@ var TestData = func() string { // Testing is an abstraction of a *testing.T. type Testing interface { - Errorf(format string, args ...interface{}) + Errorf(format string, args ...any) } // RunWithSuggestedFixes behaves like Run, but additionally applies diff --git a/go/analysis/analysistest/analysistest_test.go b/go/analysis/analysistest/analysistest_test.go index eedbb5c2a90..88cd8f8f1d5 100644 --- a/go/analysis/analysistest/analysistest_test.go +++ b/go/analysis/analysistest/analysistest_test.go @@ -262,6 +262,6 @@ type T string type errorfunc func(string) -func (f errorfunc) Errorf(format string, args ...interface{}) { +func (f errorfunc) Errorf(format string, args ...any) { f(fmt.Sprintf(format, args...)) } diff --git a/go/analysis/internal/analysisflags/flags.go b/go/analysis/internal/analysisflags/flags.go index c2445575cff..6aefef25815 100644 --- a/go/analysis/internal/analysisflags/flags.go +++ b/go/analysis/internal/analysisflags/flags.go @@ -201,7 +201,7 @@ func addVersionFlag() { type versionFlag struct{} func (versionFlag) IsBoolFlag() bool { return true } -func (versionFlag) Get() interface{} { return nil } +func (versionFlag) Get() any { return nil } func (versionFlag) String() string { return "" } func (versionFlag) Set(s string) error { if s != "full" { @@ -252,7 +252,7 @@ const ( // triState implements flag.Value, flag.Getter, and flag.boolFlag. // They work like boolean flags: we can say vet -printf as well as vet -printf=true -func (ts *triState) Get() interface{} { +func (ts *triState) Get() any { return *ts == setTrue } @@ -340,7 +340,7 @@ func PrintPlain(out io.Writer, fset *token.FileSet, contextLines int, diag analy // A JSONTree is a mapping from package ID to analysis name to result. // Each result is either a jsonError or a list of JSONDiagnostic. -type JSONTree map[string]map[string]interface{} +type JSONTree map[string]map[string]any // A TextEdit describes the replacement of a portion of a file. // Start and End are zero-based half-open indices into the original byte @@ -383,7 +383,7 @@ type JSONRelatedInformation struct { // Add adds the result of analysis 'name' on package 'id'. // The result is either a list of diagnostics or an error. func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.Diagnostic, err error) { - var v interface{} + var v any if err != nil { type jsonError struct { Err string `json:"error"` @@ -429,7 +429,7 @@ func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis. if v != nil { m, ok := tree[id] if !ok { - m = make(map[string]interface{}) + m = make(map[string]any) tree[id] = m } m[name] = v diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go index 9ec6e61cd73..7d73aa3c6bb 100644 --- a/go/analysis/internal/checker/checker_test.go +++ b/go/analysis/internal/checker/checker_test.go @@ -107,7 +107,7 @@ func NewT1() *T1 { return &T1{T} } Name: "noop", Doc: "noop", Requires: []*analysis.Analyzer{inspect.Analyzer}, - Run: func(pass *analysis.Pass) (interface{}, error) { + Run: func(pass *analysis.Pass) (any, error) { return nil, nil }, RunDespiteErrors: true, @@ -119,7 +119,7 @@ func NewT1() *T1 { return &T1{T} } Name: "noopfact", Doc: "noopfact", Requires: []*analysis.Analyzer{inspect.Analyzer}, - Run: func(pass *analysis.Pass) (interface{}, error) { + Run: func(pass *analysis.Pass) (any, error) { return nil, nil }, RunDespiteErrors: true, @@ -185,7 +185,7 @@ func TestURL(t *testing.T) { Name: "pkgname", Doc: "trivial analyzer that reports package names", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/internal/checker", - Run: func(p *analysis.Pass) (interface{}, error) { + Run: func(p *analysis.Pass) (any, error) { for _, f := range p.Files { p.ReportRangef(f.Name, "package name is %s", f.Name.Name) } diff --git a/go/analysis/internal/checker/start_test.go b/go/analysis/internal/checker/start_test.go index c78829a5adf..60ed54464ae 100644 --- a/go/analysis/internal/checker/start_test.go +++ b/go/analysis/internal/checker/start_test.go @@ -62,7 +62,7 @@ var commentAnalyzer = &analysis.Analyzer{ Run: commentRun, } -func commentRun(pass *analysis.Pass) (interface{}, error) { +func commentRun(pass *analysis.Pass) (any, error) { const ( from = "/* Package comment */" to = "// Package comment" diff --git a/go/analysis/internal/internal.go b/go/analysis/internal/internal.go index e7c8247fd33..327c4b50579 100644 --- a/go/analysis/internal/internal.go +++ b/go/analysis/internal/internal.go @@ -9,4 +9,4 @@ import "golang.org/x/tools/go/analysis" // This function is set by the checker package to provide // backdoor access to the private Pass field // of the checker.Action type, for use by analysistest. -var Pass func(interface{}) *analysis.Pass +var Pass func(any) *analysis.Pass diff --git a/go/analysis/internal/versiontest/version_test.go b/go/analysis/internal/versiontest/version_test.go index 43c52f565f7..5bd6d3027dd 100644 --- a/go/analysis/internal/versiontest/version_test.go +++ b/go/analysis/internal/versiontest/version_test.go @@ -26,7 +26,7 @@ import ( var analyzer = &analysis.Analyzer{ Name: "versiontest", Doc: "off", - Run: func(pass *analysis.Pass) (interface{}, error) { + Run: func(pass *analysis.Pass) (any, error) { pass.Reportf(pass.Files[0].Package, "goversion=%s", pass.Pkg.GoVersion()) return nil, nil }, diff --git a/go/analysis/multichecker/multichecker_test.go b/go/analysis/multichecker/multichecker_test.go index 94a280564ce..1491df153b9 100644 --- a/go/analysis/multichecker/multichecker_test.go +++ b/go/analysis/multichecker/multichecker_test.go @@ -23,7 +23,7 @@ func main() { fail := &analysis.Analyzer{ Name: "fail", Doc: "always fail on a package 'sort'", - Run: func(pass *analysis.Pass) (interface{}, error) { + Run: func(pass *analysis.Pass) (any, error) { if pass.Pkg.Path() == "sort" { return nil, fmt.Errorf("failed") } diff --git a/go/analysis/passes/appends/appends.go b/go/analysis/passes/appends/appends.go index 6976f0d9090..e554c3cc903 100644 --- a/go/analysis/passes/appends/appends.go +++ b/go/analysis/passes/appends/appends.go @@ -29,7 +29,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ diff --git a/go/analysis/passes/asmdecl/asmdecl.go b/go/analysis/passes/asmdecl/asmdecl.go index a47ecbae731..436b03cb290 100644 --- a/go/analysis/passes/asmdecl/asmdecl.go +++ b/go/analysis/passes/asmdecl/asmdecl.go @@ -150,7 +150,7 @@ var ( abiSuff = re(`^(.+)<(ABI.+)>$`) ) -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { // No work if no assembly files. var sfiles []string for _, fname := range pass.OtherFiles { @@ -226,7 +226,7 @@ Files: for lineno, line := range lines { lineno++ - badf := func(format string, args ...interface{}) { + badf := func(format string, args ...any) { pass.Reportf(analysisutil.LineStart(tf, lineno), "[%s] %s: %s", arch, fnName, fmt.Sprintf(format, args...)) } @@ -646,7 +646,7 @@ func asmParseDecl(pass *analysis.Pass, decl *ast.FuncDecl) map[string]*asmFunc { } // asmCheckVar checks a single variable reference. -func asmCheckVar(badf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar, archDef *asmArch) { +func asmCheckVar(badf func(string, ...any), fn *asmFunc, line, expr string, off int, v *asmVar, archDef *asmArch) { m := asmOpcode.FindStringSubmatch(line) if m == nil { if !strings.HasPrefix(strings.TrimSpace(line), "//") { diff --git a/go/analysis/passes/buildssa/buildssa.go b/go/analysis/passes/buildssa/buildssa.go index f077ea28247..f49fea51762 100644 --- a/go/analysis/passes/buildssa/buildssa.go +++ b/go/analysis/passes/buildssa/buildssa.go @@ -32,7 +32,7 @@ type SSA struct { SrcFuncs []*ssa.Function } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { // We must create a new Program for each Package because the // analysis API provides no place to hang a Program shared by // all Packages. Consequently, SSA Packages and Functions do not diff --git a/go/analysis/passes/buildtag/buildtag.go b/go/analysis/passes/buildtag/buildtag.go index e7434e8fed2..6c7a0df585d 100644 --- a/go/analysis/passes/buildtag/buildtag.go +++ b/go/analysis/passes/buildtag/buildtag.go @@ -26,7 +26,7 @@ var Analyzer = &analysis.Analyzer{ Run: runBuildTag, } -func runBuildTag(pass *analysis.Pass) (interface{}, error) { +func runBuildTag(pass *analysis.Pass) (any, error) { for _, f := range pass.Files { checkGoFile(pass, f) } diff --git a/go/analysis/passes/cgocall/cgocall.go b/go/analysis/passes/cgocall/cgocall.go index 4f3bb035d65..d9189b5b696 100644 --- a/go/analysis/passes/cgocall/cgocall.go +++ b/go/analysis/passes/cgocall/cgocall.go @@ -55,7 +55,7 @@ func run(pass *analysis.Pass) (any, error) { return nil, nil } -func checkCgo(fset *token.FileSet, f *ast.File, info *types.Info, reportf func(token.Pos, string, ...interface{})) { +func checkCgo(fset *token.FileSet, f *ast.File, info *types.Info, reportf func(token.Pos, string, ...any)) { ast.Inspect(f, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { diff --git a/go/analysis/passes/composite/composite.go b/go/analysis/passes/composite/composite.go index f56c3e622fb..60c6afe49f0 100644 --- a/go/analysis/passes/composite/composite.go +++ b/go/analysis/passes/composite/composite.go @@ -51,7 +51,7 @@ func init() { // runUnkeyedLiteral checks if a composite literal is a struct literal with // unkeyed fields. -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go index 8a215677165..49c14d4980d 100644 --- a/go/analysis/passes/copylock/copylock.go +++ b/go/analysis/passes/copylock/copylock.go @@ -36,7 +36,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) var goversion string // effective file version ("" => unknown) diff --git a/go/analysis/passes/ctrlflow/ctrlflow.go b/go/analysis/passes/ctrlflow/ctrlflow.go index d21adeee900..951aaed00fd 100644 --- a/go/analysis/passes/ctrlflow/ctrlflow.go +++ b/go/analysis/passes/ctrlflow/ctrlflow.go @@ -80,7 +80,7 @@ func (c *CFGs) FuncLit(lit *ast.FuncLit) *cfg.CFG { return c.funcLits[lit].cfg } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) // Because CFG construction consumes and produces noReturn diff --git a/go/analysis/passes/directive/directive.go b/go/analysis/passes/directive/directive.go index b205402388e..bebec891408 100644 --- a/go/analysis/passes/directive/directive.go +++ b/go/analysis/passes/directive/directive.go @@ -40,7 +40,7 @@ var Analyzer = &analysis.Analyzer{ Run: runDirective, } -func runDirective(pass *analysis.Pass) (interface{}, error) { +func runDirective(pass *analysis.Pass) (any, error) { for _, f := range pass.Files { checkGoFile(pass, f) } diff --git a/go/analysis/passes/fieldalignment/fieldalignment.go b/go/analysis/passes/fieldalignment/fieldalignment.go index 93fa39140e6..e2ddc83b604 100644 --- a/go/analysis/passes/fieldalignment/fieldalignment.go +++ b/go/analysis/passes/fieldalignment/fieldalignment.go @@ -65,7 +65,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.StructType)(nil), diff --git a/go/analysis/passes/findcall/findcall.go b/go/analysis/passes/findcall/findcall.go index 2671573d1fe..9db4de1c20f 100644 --- a/go/analysis/passes/findcall/findcall.go +++ b/go/analysis/passes/findcall/findcall.go @@ -38,7 +38,7 @@ func init() { Analyzer.Flags.StringVar(&name, "name", name, "name of the function to find") } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { for _, f := range pass.Files { ast.Inspect(f, func(n ast.Node) bool { if call, ok := n.(*ast.CallExpr); ok { diff --git a/go/analysis/passes/framepointer/framepointer.go b/go/analysis/passes/framepointer/framepointer.go index 8012de99daa..ba94fd68ea4 100644 --- a/go/analysis/passes/framepointer/framepointer.go +++ b/go/analysis/passes/framepointer/framepointer.go @@ -113,7 +113,7 @@ var arm64Branch = map[string]bool{ "RET": true, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { arch, ok := arches[build.Default.GOARCH] if !ok { return nil, nil diff --git a/go/analysis/passes/ifaceassert/ifaceassert.go b/go/analysis/passes/ifaceassert/ifaceassert.go index 5f07ed3ffde..4022dbe7c22 100644 --- a/go/analysis/passes/ifaceassert/ifaceassert.go +++ b/go/analysis/passes/ifaceassert/ifaceassert.go @@ -52,7 +52,7 @@ func assertableTo(free *typeparams.Free, v, t types.Type) *types.Func { return nil } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.TypeAssertExpr)(nil), diff --git a/go/analysis/passes/inspect/inspect.go b/go/analysis/passes/inspect/inspect.go index 3b121cb0ce7..ee1972f56df 100644 --- a/go/analysis/passes/inspect/inspect.go +++ b/go/analysis/passes/inspect/inspect.go @@ -44,6 +44,6 @@ var Analyzer = &analysis.Analyzer{ ResultType: reflect.TypeOf(new(inspector.Inspector)), } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { return inspector.New(pass.Files), nil } diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index d3181242153..64df1b106a1 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -30,7 +30,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ diff --git a/go/analysis/passes/lostcancel/lostcancel.go b/go/analysis/passes/lostcancel/lostcancel.go index f8a661aa5db..a7fee180925 100644 --- a/go/analysis/passes/lostcancel/lostcancel.go +++ b/go/analysis/passes/lostcancel/lostcancel.go @@ -47,7 +47,7 @@ var contextPackage = "context" // containing the assignment, we assume that other uses exist. // // checkLostCancel analyzes a single named or literal function. -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { // Fast path: bypass check if file doesn't use context.WithCancel. if !analysisinternal.Imports(pass.Pkg, contextPackage) { return nil, nil diff --git a/go/analysis/passes/nilfunc/nilfunc.go b/go/analysis/passes/nilfunc/nilfunc.go index 778f7f1f8f9..3ac2dcd4907 100644 --- a/go/analysis/passes/nilfunc/nilfunc.go +++ b/go/analysis/passes/nilfunc/nilfunc.go @@ -30,7 +30,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ diff --git a/go/analysis/passes/nilness/nilness.go b/go/analysis/passes/nilness/nilness.go index faaf12a9385..af61ae6088d 100644 --- a/go/analysis/passes/nilness/nilness.go +++ b/go/analysis/passes/nilness/nilness.go @@ -28,7 +28,7 @@ var Analyzer = &analysis.Analyzer{ Requires: []*analysis.Analyzer{buildssa.Analyzer}, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) for _, fn := range ssainput.SrcFuncs { runFunc(pass, fn) @@ -37,7 +37,7 @@ func run(pass *analysis.Pass) (interface{}, error) { } func runFunc(pass *analysis.Pass, fn *ssa.Function) { - reportf := func(category string, pos token.Pos, format string, args ...interface{}) { + reportf := func(category string, pos token.Pos, format string, args ...any) { // We ignore nil-checking ssa.Instructions // that don't correspond to syntax. if pos.IsValid() { diff --git a/go/analysis/passes/pkgfact/pkgfact.go b/go/analysis/passes/pkgfact/pkgfact.go index 077c8780815..31748795dac 100644 --- a/go/analysis/passes/pkgfact/pkgfact.go +++ b/go/analysis/passes/pkgfact/pkgfact.go @@ -53,7 +53,7 @@ type pairsFact []string func (f *pairsFact) AFact() {} func (f *pairsFact) String() string { return "pairs(" + strings.Join(*f, ", ") + ")" } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { result := make(map[string]string) // At each import, print the fact from the imported diff --git a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go index 72435b2fc7a..d0632dbdafe 100644 --- a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go +++ b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go @@ -28,7 +28,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ diff --git a/go/analysis/passes/shadow/shadow.go b/go/analysis/passes/shadow/shadow.go index 30258c991f3..8f768bb76c5 100644 --- a/go/analysis/passes/shadow/shadow.go +++ b/go/analysis/passes/shadow/shadow.go @@ -36,7 +36,7 @@ func init() { Analyzer.Flags.BoolVar(&strict, "strict", strict, "whether to be strict about shadowing; can be noisy") } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) spans := make(map[types.Object]span) diff --git a/go/analysis/passes/shift/shift.go b/go/analysis/passes/shift/shift.go index 46b5f6d68c6..57987b3d203 100644 --- a/go/analysis/passes/shift/shift.go +++ b/go/analysis/passes/shift/shift.go @@ -34,7 +34,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) // Do a complete pass to compute dead nodes. diff --git a/go/analysis/passes/stdmethods/stdmethods.go b/go/analysis/passes/stdmethods/stdmethods.go index 28f51b1ec9a..a0bdf001abd 100644 --- a/go/analysis/passes/stdmethods/stdmethods.go +++ b/go/analysis/passes/stdmethods/stdmethods.go @@ -66,7 +66,7 @@ var canonicalMethods = map[string]struct{ args, results []string }{ "WriteTo": {[]string{"=io.Writer"}, []string{"int64", "error"}}, // io.WriterTo } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ diff --git a/go/analysis/passes/stringintconv/string.go b/go/analysis/passes/stringintconv/string.go index f56e6ecaa29..a23721cd26f 100644 --- a/go/analysis/passes/stringintconv/string.go +++ b/go/analysis/passes/stringintconv/string.go @@ -70,7 +70,7 @@ func typeName(t types.Type) string { return "" } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.File)(nil), diff --git a/go/analysis/passes/structtag/structtag.go b/go/analysis/passes/structtag/structtag.go index 4115ef76943..d926503403d 100644 --- a/go/analysis/passes/structtag/structtag.go +++ b/go/analysis/passes/structtag/structtag.go @@ -34,7 +34,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ diff --git a/go/analysis/passes/testinggoroutine/testinggoroutine.go b/go/analysis/passes/testinggoroutine/testinggoroutine.go index fef5a6014c4..f49ac4eb1a0 100644 --- a/go/analysis/passes/testinggoroutine/testinggoroutine.go +++ b/go/analysis/passes/testinggoroutine/testinggoroutine.go @@ -36,7 +36,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) if !analysisinternal.Imports(pass.Pkg, "testing") { diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go index 285b34218c3..9f59006ebb2 100644 --- a/go/analysis/passes/tests/tests.go +++ b/go/analysis/passes/tests/tests.go @@ -47,7 +47,7 @@ var acceptedFuzzTypes = []types.Type{ types.NewSlice(types.Universe.Lookup("byte").Type()), } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { for _, f := range pass.Files { if !strings.HasSuffix(pass.Fset.File(f.FileStart).Name(), "_test.go") { continue diff --git a/go/analysis/passes/unreachable/unreachable.go b/go/analysis/passes/unreachable/unreachable.go index b810db7ee95..fcf5fbd9060 100644 --- a/go/analysis/passes/unreachable/unreachable.go +++ b/go/analysis/passes/unreachable/unreachable.go @@ -30,7 +30,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ diff --git a/go/analysis/passes/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go index fb5b944faad..57c6da64ff3 100644 --- a/go/analysis/passes/unsafeptr/unsafeptr.go +++ b/go/analysis/passes/unsafeptr/unsafeptr.go @@ -30,7 +30,7 @@ var Analyzer = &analysis.Analyzer{ Run: run, } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go index e298f644277..932f1347e56 100644 --- a/go/analysis/passes/unusedresult/unusedresult.go +++ b/go/analysis/passes/unusedresult/unusedresult.go @@ -85,7 +85,7 @@ func init() { "comma-separated list of names of methods of type func() string whose results must be used") } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) // Split functions into (pkg, name) pairs to save allocation later. diff --git a/go/analysis/passes/usesgenerics/usesgenerics.go b/go/analysis/passes/usesgenerics/usesgenerics.go index 5c5df3a79a0..b7ff3ad6877 100644 --- a/go/analysis/passes/usesgenerics/usesgenerics.go +++ b/go/analysis/passes/usesgenerics/usesgenerics.go @@ -53,7 +53,7 @@ type featuresFact struct { func (f *featuresFact) AFact() {} func (f *featuresFact) String() string { return f.Features.String() } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) direct := genericfeatures.ForPackage(inspect, pass.TypesInfo) diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go index 82c3db6a39d..a1ee80388b6 100644 --- a/go/analysis/unitchecker/unitchecker.go +++ b/go/analysis/unitchecker/unitchecker.go @@ -287,7 +287,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re // Also build a map to hold working state and result. type action struct { once sync.Once - result interface{} + result any err error usesFacts bool // (transitively uses) diagnostics []analysis.Diagnostic @@ -337,7 +337,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re // The inputs to this analysis are the // results of its prerequisites. - inputs := make(map[*analysis.Analyzer]interface{}) + inputs := make(map[*analysis.Analyzer]any) var failed []string for _, req := range a.Requires { reqact := exec(req) diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go index 173d76348f7..6c3bba6793e 100644 --- a/go/analysis/unitchecker/unitchecker_test.go +++ b/go/analysis/unitchecker/unitchecker_test.go @@ -59,7 +59,7 @@ func testIntegration(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a func _() { diff --git a/go/analysis/validate_test.go b/go/analysis/validate_test.go index 7f4ee2c05b9..b192ef0a3c0 100644 --- a/go/analysis/validate_test.go +++ b/go/analysis/validate_test.go @@ -11,7 +11,7 @@ import ( func TestValidate(t *testing.T) { var ( - run = func(p *Pass) (interface{}, error) { + run = func(p *Pass) (any, error) { return nil, nil } dependsOnSelf = &Analyzer{ @@ -130,7 +130,7 @@ func TestCycleInRequiresGraphErrorMessage(t *testing.T) { func TestValidateEmptyDoc(t *testing.T) { withoutDoc := &Analyzer{ Name: "withoutDoc", - Run: func(p *Pass) (interface{}, error) { + Run: func(p *Pass) (any, error) { return nil, nil }, } diff --git a/go/buildutil/fakecontext.go b/go/buildutil/fakecontext.go index 763d18809b4..1f75141d504 100644 --- a/go/buildutil/fakecontext.go +++ b/go/buildutil/fakecontext.go @@ -95,7 +95,7 @@ func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() } type fakeFileInfo string func (fi fakeFileInfo) Name() string { return string(fi) } -func (fakeFileInfo) Sys() interface{} { return nil } +func (fakeFileInfo) Sys() any { return nil } func (fakeFileInfo) ModTime() time.Time { return time.Time{} } func (fakeFileInfo) IsDir() bool { return false } func (fakeFileInfo) Size() int64 { return 0 } @@ -104,7 +104,7 @@ func (fakeFileInfo) Mode() os.FileMode { return 0644 } type fakeDirInfo string func (fd fakeDirInfo) Name() string { return string(fd) } -func (fakeDirInfo) Sys() interface{} { return nil } +func (fakeDirInfo) Sys() any { return nil } func (fakeDirInfo) ModTime() time.Time { return time.Time{} } func (fakeDirInfo) IsDir() bool { return true } func (fakeDirInfo) Size() int64 { return 0 } diff --git a/go/buildutil/tags.go b/go/buildutil/tags.go index 32c8d1424d2..410c8e72d48 100644 --- a/go/buildutil/tags.go +++ b/go/buildutil/tags.go @@ -51,7 +51,7 @@ func (v *TagsFlag) Set(s string) error { return nil } -func (v *TagsFlag) Get() interface{} { return *v } +func (v *TagsFlag) Get() any { return *v } func splitQuotedFields(s string) ([]string, error) { // See $GOROOT/src/cmd/internal/quoted/quoted.go (Split) diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go index b489b0178c8..224c0b96ce0 100644 --- a/go/callgraph/rta/rta.go +++ b/go/callgraph/rta/rta.go @@ -371,7 +371,7 @@ func (r *rta) interfaces(C types.Type) []*types.Interface { // Ascertain set of interfaces C implements // and update the 'implements' relation. - r.interfaceTypes.Iterate(func(I types.Type, v interface{}) { + r.interfaceTypes.Iterate(func(I types.Type, v any) { iinfo := v.(*interfaceTypeInfo) if I := types.Unalias(I).(*types.Interface); implements(cinfo, iinfo) { iinfo.implementations = append(iinfo.implementations, C) @@ -400,7 +400,7 @@ func (r *rta) implementations(I *types.Interface) []types.Type { // Ascertain set of concrete types that implement I // and update the 'implements' relation. - r.concreteTypes.Iterate(func(C types.Type, v interface{}) { + r.concreteTypes.Iterate(func(C types.Type, v any) { cinfo := v.(*concreteTypeInfo) if implements(cinfo, iinfo) { cinfo.implements = append(cinfo.implements, I) diff --git a/go/callgraph/rta/rta_test.go b/go/callgraph/rta/rta_test.go index dcec98d7c5d..6b16484245b 100644 --- a/go/callgraph/rta/rta_test.go +++ b/go/callgraph/rta/rta_test.go @@ -220,7 +220,7 @@ func check(t *testing.T, f *ast.File, pkg *ssa.Package, res *rta.Result) { // Check runtime types. { got := make(stringset) - res.RuntimeTypes.Iterate(func(key types.Type, value interface{}) { + res.RuntimeTypes.Iterate(func(key types.Type, value any) { if !value.(bool) { // accessible to reflection typ := types.TypeString(types.Unalias(key), types.RelativeTo(pkg.Pkg)) got[typ] = true diff --git a/go/callgraph/vta/internal/trie/builder.go b/go/callgraph/vta/internal/trie/builder.go index c814c039f72..bdd39397ec6 100644 --- a/go/callgraph/vta/internal/trie/builder.go +++ b/go/callgraph/vta/internal/trie/builder.go @@ -14,13 +14,13 @@ package trie // // Collisions functions may be applied whenever a value is inserted // or two maps are merged, or intersected. -type Collision func(lhs interface{}, rhs interface{}) interface{} +type Collision func(lhs any, rhs any) any // TakeLhs always returns the left value in a collision. -func TakeLhs(lhs, rhs interface{}) interface{} { return lhs } +func TakeLhs(lhs, rhs any) any { return lhs } // TakeRhs always returns the right hand side in a collision. -func TakeRhs(lhs, rhs interface{}) interface{} { return rhs } +func TakeRhs(lhs, rhs any) any { return rhs } // Builder creates new Map. Each Builder has a unique Scope. // @@ -78,7 +78,7 @@ func (b *Builder) Empty() Map { return Map{b.Scope(), b.empty} } // if _, ok := m[k]; ok { m[k] = c(m[k], v} else { m[k] = v} // // An insertion or update happened whenever Insert(m, ...) != m . -func (b *Builder) InsertWith(c Collision, m Map, k uint64, v interface{}) Map { +func (b *Builder) InsertWith(c Collision, m Map, k uint64, v any) Map { m = b.Clone(m) return Map{b.Scope(), b.insert(c, m.n, b.mkLeaf(key(k), v), false)} } @@ -92,7 +92,7 @@ func (b *Builder) InsertWith(c Collision, m Map, k uint64, v interface{}) Map { // if _, ok := m[k]; ok { m[k] = val } // // This is equivalent to b.Merge(m, b.Create({k: v})). -func (b *Builder) Insert(m Map, k uint64, v interface{}) Map { +func (b *Builder) Insert(m Map, k uint64, v any) Map { return b.InsertWith(TakeLhs, m, k, v) } @@ -100,7 +100,7 @@ func (b *Builder) Insert(m Map, k uint64, v interface{}) Map { // updating a map[uint64]interface{} by: // // m[key] = val -func (b *Builder) Update(m Map, key uint64, val interface{}) Map { +func (b *Builder) Update(m Map, key uint64, val any) Map { return b.InsertWith(TakeRhs, m, key, val) } @@ -185,14 +185,14 @@ func (b *Builder) MutEmpty() MutMap { // Insert an element into the map using the collision function for the builder. // Returns true if the element was inserted. -func (mm *MutMap) Insert(k uint64, v interface{}) bool { +func (mm *MutMap) Insert(k uint64, v any) bool { old := mm.M mm.M = mm.B.Insert(old, k, v) return old != mm.M } // Updates an element in the map. Returns true if the map was updated. -func (mm *MutMap) Update(k uint64, v interface{}) bool { +func (mm *MutMap) Update(k uint64, v any) bool { old := mm.M mm.M = mm.B.Update(old, k, v) return old != mm.M @@ -221,7 +221,7 @@ func (mm *MutMap) Intersect(other Map) bool { return old != mm.M } -func (b *Builder) Create(m map[uint64]interface{}) Map { +func (b *Builder) Create(m map[uint64]any) Map { var leaves []*leaf for k, v := range m { leaves = append(leaves, b.mkLeaf(key(k), v)) @@ -259,7 +259,7 @@ func (b *Builder) create(leaves []*leaf) node { } // mkLeaf returns the hash-consed representative of (k, v) in the current scope. -func (b *Builder) mkLeaf(k key, v interface{}) *leaf { +func (b *Builder) mkLeaf(k key, v any) *leaf { rep, ok := b.leaves[leaf{k, v}] if !ok { rep = &leaf{k, v} // heap-allocated copy diff --git a/go/callgraph/vta/internal/trie/op_test.go b/go/callgraph/vta/internal/trie/op_test.go index ba0d5be71a9..b4610d55c22 100644 --- a/go/callgraph/vta/internal/trie/op_test.go +++ b/go/callgraph/vta/internal/trie/op_test.go @@ -21,13 +21,13 @@ import ( // mapCollection is effectively a []map[uint64]interface{}. // These support operations being applied to the i'th maps. type mapCollection interface { - Elements() []map[uint64]interface{} + Elements() []map[uint64]any DeepEqual(l, r int) bool - Lookup(id int, k uint64) (interface{}, bool) + Lookup(id int, k uint64) (any, bool) - Insert(id int, k uint64, v interface{}) - Update(id int, k uint64, v interface{}) + Insert(id int, k uint64, v any) + Update(id int, k uint64, v any) Remove(id int, k uint64) Intersect(l int, r int) Merge(l int, r int) @@ -86,19 +86,19 @@ type trieCollection struct { tries []trie.MutMap } -func (c *trieCollection) Elements() []map[uint64]interface{} { - var maps []map[uint64]interface{} +func (c *trieCollection) Elements() []map[uint64]any { + var maps []map[uint64]any for _, m := range c.tries { maps = append(maps, trie.Elems(m.M)) } return maps } -func (c *trieCollection) Eq(id int, m map[uint64]interface{}) bool { +func (c *trieCollection) Eq(id int, m map[uint64]any) bool { elems := trie.Elems(c.tries[id].M) return !reflect.DeepEqual(elems, m) } -func (c *trieCollection) Lookup(id int, k uint64) (interface{}, bool) { +func (c *trieCollection) Lookup(id int, k uint64) (any, bool) { return c.tries[id].M.Lookup(k) } func (c *trieCollection) DeepEqual(l, r int) bool { @@ -109,11 +109,11 @@ func (c *trieCollection) Add() { c.tries = append(c.tries, c.b.MutEmpty()) } -func (c *trieCollection) Insert(id int, k uint64, v interface{}) { +func (c *trieCollection) Insert(id int, k uint64, v any) { c.tries[id].Insert(k, v) } -func (c *trieCollection) Update(id int, k uint64, v interface{}) { +func (c *trieCollection) Update(id int, k uint64, v any) { c.tries[id].Update(k, v) } @@ -140,7 +140,7 @@ func (c *trieCollection) Assign(l, r int) { c.tries[l] = c.tries[r] } -func average(x interface{}, y interface{}) interface{} { +func average(x any, y any) any { if x, ok := x.(float32); ok { if y, ok := y.(float32); ok { return (x + y) / 2.0 @@ -149,13 +149,13 @@ func average(x interface{}, y interface{}) interface{} { return x } -type builtinCollection []map[uint64]interface{} +type builtinCollection []map[uint64]any -func (c builtinCollection) Elements() []map[uint64]interface{} { +func (c builtinCollection) Elements() []map[uint64]any { return c } -func (c builtinCollection) Lookup(id int, k uint64) (interface{}, bool) { +func (c builtinCollection) Lookup(id int, k uint64) (any, bool) { v, ok := c[id][k] return v, ok } @@ -163,13 +163,13 @@ func (c builtinCollection) DeepEqual(l, r int) bool { return reflect.DeepEqual(c[l], c[r]) } -func (c builtinCollection) Insert(id int, k uint64, v interface{}) { +func (c builtinCollection) Insert(id int, k uint64, v any) { if _, ok := c[id][k]; !ok { c[id][k] = v } } -func (c builtinCollection) Update(id int, k uint64, v interface{}) { +func (c builtinCollection) Update(id int, k uint64, v any) { c[id][k] = v } @@ -178,7 +178,7 @@ func (c builtinCollection) Remove(id int, k uint64) { } func (c builtinCollection) Intersect(l int, r int) { - result := map[uint64]interface{}{} + result := map[uint64]any{} for k, v := range c[l] { if _, ok := c[r][k]; ok { result[k] = v @@ -188,7 +188,7 @@ func (c builtinCollection) Intersect(l int, r int) { } func (c builtinCollection) Merge(l int, r int) { - result := map[uint64]interface{}{} + result := map[uint64]any{} for k, v := range c[r] { result[k] = v } @@ -199,7 +199,7 @@ func (c builtinCollection) Merge(l int, r int) { } func (c builtinCollection) Average(l int, r int) { - avg := map[uint64]interface{}{} + avg := map[uint64]any{} for k, lv := range c[l] { if rv, ok := c[r][k]; ok { avg[k] = average(lv, rv) @@ -216,7 +216,7 @@ func (c builtinCollection) Average(l int, r int) { } func (c builtinCollection) Assign(l, r int) { - m := map[uint64]interface{}{} + m := map[uint64]any{} for k, v := range c[r] { m[k] = v } @@ -224,7 +224,7 @@ func (c builtinCollection) Assign(l, r int) { } func (c builtinCollection) Clear(id int) { - c[id] = map[uint64]interface{}{} + c[id] = map[uint64]any{} } func newTriesCollection(size int) *trieCollection { @@ -241,7 +241,7 @@ func newTriesCollection(size int) *trieCollection { func newMapsCollection(size int) *builtinCollection { maps := make(builtinCollection, size) for i := 0; i < size; i++ { - maps[i] = map[uint64]interface{}{} + maps[i] = map[uint64]any{} } return &maps } @@ -255,9 +255,9 @@ type operation struct { } // Apply the operation to maps. -func (op operation) Apply(maps mapCollection) interface{} { +func (op operation) Apply(maps mapCollection) any { type lookupresult struct { - v interface{} + v any ok bool } switch op.code { diff --git a/go/callgraph/vta/internal/trie/trie.go b/go/callgraph/vta/internal/trie/trie.go index 511fde51565..a8480192556 100644 --- a/go/callgraph/vta/internal/trie/trie.go +++ b/go/callgraph/vta/internal/trie/trie.go @@ -55,7 +55,7 @@ func (m Map) Size() int { } return m.n.size() } -func (m Map) Lookup(k uint64) (interface{}, bool) { +func (m Map) Lookup(k uint64) (any, bool) { if m.n != nil { if leaf := m.n.find(key(k)); leaf != nil { return leaf.v, true @@ -68,7 +68,7 @@ func (m Map) Lookup(k uint64) (interface{}, bool) { // %s string conversion for . func (m Map) String() string { var kvs []string - m.Range(func(u uint64, i interface{}) bool { + m.Range(func(u uint64, i any) bool { kvs = append(kvs, fmt.Sprintf("%d: %s", u, i)) return true }) @@ -78,7 +78,7 @@ func (m Map) String() string { // Range over the leaf (key, value) pairs in the map in order and // applies cb(key, value) to each. Stops early if cb returns false. // Returns true if all elements were visited without stopping early. -func (m Map) Range(cb func(uint64, interface{}) bool) bool { +func (m Map) Range(cb func(uint64, any) bool) bool { if m.n != nil { return m.n.visit(cb) } @@ -100,9 +100,9 @@ func (m Map) DeepEqual(other Map) bool { } // Elems are the (k,v) elements in the Map as a map[uint64]interface{} -func Elems(m Map) map[uint64]interface{} { - dest := make(map[uint64]interface{}, m.Size()) - m.Range(func(k uint64, v interface{}) bool { +func Elems(m Map) map[uint64]any { + dest := make(map[uint64]any, m.Size()) + m.Range(func(k uint64, v any) bool { dest[k] = v return true }) @@ -117,7 +117,7 @@ type node interface { // visit the leaves (key, value) pairs in the map in order and // applies cb(key, value) to each. Stops early if cb returns false. // Returns true if all elements were visited without stopping early. - visit(cb func(uint64, interface{}) bool) bool + visit(cb func(uint64, any) bool) bool // Two nodes contain the same elements regardless of scope. deepEqual(node) bool @@ -139,7 +139,7 @@ type empty struct { // leaf represents a single pair. type leaf struct { k key - v interface{} + v any } // branch represents a tree node within the Patricia trie. @@ -215,13 +215,13 @@ func (br *branch) deepEqual(m node) bool { return false } -func (*empty) visit(cb func(uint64, interface{}) bool) bool { +func (*empty) visit(cb func(uint64, any) bool) bool { return true } -func (l *leaf) visit(cb func(uint64, interface{}) bool) bool { +func (l *leaf) visit(cb func(uint64, any) bool) bool { return cb(uint64(l.k), l.v) } -func (br *branch) visit(cb func(uint64, interface{}) bool) bool { +func (br *branch) visit(cb func(uint64, any) bool) bool { if !br.left.visit(cb) { return false } diff --git a/go/callgraph/vta/internal/trie/trie_test.go b/go/callgraph/vta/internal/trie/trie_test.go index 71fd398f12c..817cb5c5e28 100644 --- a/go/callgraph/vta/internal/trie/trie_test.go +++ b/go/callgraph/vta/internal/trie/trie_test.go @@ -34,8 +34,8 @@ func TestScope(t *testing.T) { } func TestCollision(t *testing.T) { - var x interface{} = 1 - var y interface{} = 2 + var x any = 1 + var y any = 2 if v := TakeLhs(x, y); v != x { t.Errorf("TakeLhs(%s, %s) got %s. want %s", x, y, v, x) @@ -57,7 +57,7 @@ func TestDefault(t *testing.T) { if v, ok := def.Lookup(123); !(v == nil && !ok) { t.Errorf("Scope{}.Lookup() = (%s, %v) not (nil, false)", v, ok) } - if !def.Range(func(k uint64, v interface{}) bool { + if !def.Range(func(k uint64, v any) bool { t.Errorf("Scope{}.Range() called it callback on %d:%s", k, v) return true }) { @@ -114,7 +114,7 @@ func TestEmpty(t *testing.T) { if l := e.n.find(123); l != nil { t.Errorf("empty.find(123) got %v. want nil", l) } - e.Range(func(k uint64, v interface{}) bool { + e.Range(func(k uint64, v any) bool { t.Errorf("empty.Range() called it callback on %d:%s", k, v) return true }) @@ -129,23 +129,23 @@ func TestCreate(t *testing.T) { // The node orders are printed in lexicographic little-endian. b := NewBuilder() for _, c := range []struct { - m map[uint64]interface{} + m map[uint64]any want string }{ { - map[uint64]interface{}{}, + map[uint64]any{}, "{}", }, { - map[uint64]interface{}{1: "a"}, + map[uint64]any{1: "a"}, "{1: a}", }, { - map[uint64]interface{}{2: "b", 1: "a"}, + map[uint64]any{2: "b", 1: "a"}, "{1: a, 2: b}", }, { - map[uint64]interface{}{1: "x", 4: "y", 5: "z"}, + map[uint64]any{1: "x", 4: "y", 5: "z"}, "{1: x, 4: y, 5: z}", }, } { @@ -158,7 +158,7 @@ func TestCreate(t *testing.T) { func TestElems(t *testing.T) { b := NewBuilder() - for _, orig := range []map[uint64]interface{}{ + for _, orig := range []map[uint64]any{ {}, {1: "a"}, {1: "a", 2: "b"}, @@ -174,10 +174,10 @@ func TestElems(t *testing.T) { func TestRange(t *testing.T) { b := NewBuilder() - m := b.Create(map[uint64]interface{}{1: "x", 3: "y", 5: "z", 6: "stop", 8: "a"}) + m := b.Create(map[uint64]any{1: "x", 3: "y", 5: "z", 6: "stop", 8: "a"}) calls := 0 - cb := func(k uint64, v interface{}) bool { + cb := func(k uint64, v any) bool { t.Logf("visiting (%d, %v)", k, v) calls++ return k%2 != 0 // stop after the first even number. @@ -195,7 +195,7 @@ func TestRange(t *testing.T) { } func TestDeepEqual(t *testing.T) { - for _, m := range []map[uint64]interface{}{ + for _, m := range []map[uint64]any{ {}, {1: "x"}, {1: "x", 2: "y"}, @@ -210,32 +210,32 @@ func TestDeepEqual(t *testing.T) { func TestNotDeepEqual(t *testing.T) { for _, c := range []struct { - left map[uint64]interface{} - right map[uint64]interface{} + left map[uint64]any + right map[uint64]any }{ { - map[uint64]interface{}{1: "x"}, - map[uint64]interface{}{}, + map[uint64]any{1: "x"}, + map[uint64]any{}, }, { - map[uint64]interface{}{}, - map[uint64]interface{}{1: "y"}, + map[uint64]any{}, + map[uint64]any{1: "y"}, }, { - map[uint64]interface{}{1: "x"}, - map[uint64]interface{}{1: "y"}, + map[uint64]any{1: "x"}, + map[uint64]any{1: "y"}, }, { - map[uint64]interface{}{1: "x"}, - map[uint64]interface{}{1: "x", 2: "Y"}, + map[uint64]any{1: "x"}, + map[uint64]any{1: "x", 2: "Y"}, }, { - map[uint64]interface{}{1: "x", 2: "Y"}, - map[uint64]interface{}{1: "x"}, + map[uint64]any{1: "x", 2: "Y"}, + map[uint64]any{1: "x"}, }, { - map[uint64]interface{}{1: "x", 2: "y"}, - map[uint64]interface{}{1: "x", 2: "Y"}, + map[uint64]any{1: "x", 2: "y"}, + map[uint64]any{1: "x", 2: "Y"}, }, } { l := NewBuilder().Create(c.left) @@ -249,97 +249,97 @@ func TestNotDeepEqual(t *testing.T) { func TestMerge(t *testing.T) { b := NewBuilder() for _, c := range []struct { - left map[uint64]interface{} - right map[uint64]interface{} + left map[uint64]any + right map[uint64]any want string }{ { - map[uint64]interface{}{}, - map[uint64]interface{}{}, + map[uint64]any{}, + map[uint64]any{}, "{}", }, { - map[uint64]interface{}{}, - map[uint64]interface{}{1: "a"}, + map[uint64]any{}, + map[uint64]any{1: "a"}, "{1: a}", }, { - map[uint64]interface{}{1: "a"}, - map[uint64]interface{}{}, + map[uint64]any{1: "a"}, + map[uint64]any{}, "{1: a}", }, { - map[uint64]interface{}{1: "a", 2: "b"}, - map[uint64]interface{}{}, + map[uint64]any{1: "a", 2: "b"}, + map[uint64]any{}, "{1: a, 2: b}", }, { - map[uint64]interface{}{1: "x"}, - map[uint64]interface{}{1: "y"}, + map[uint64]any{1: "x"}, + map[uint64]any{1: "y"}, "{1: x}", // default collision is left }, { - map[uint64]interface{}{1: "x"}, - map[uint64]interface{}{2: "y"}, + map[uint64]any{1: "x"}, + map[uint64]any{2: "y"}, "{1: x, 2: y}", }, { - map[uint64]interface{}{4: "y", 5: "z"}, - map[uint64]interface{}{1: "x"}, + map[uint64]any{4: "y", 5: "z"}, + map[uint64]any{1: "x"}, "{1: x, 4: y, 5: z}", }, { - map[uint64]interface{}{1: "x", 5: "z"}, - map[uint64]interface{}{4: "y"}, + map[uint64]any{1: "x", 5: "z"}, + map[uint64]any{4: "y"}, "{1: x, 4: y, 5: z}", }, { - map[uint64]interface{}{1: "x", 4: "y"}, - map[uint64]interface{}{5: "z"}, + map[uint64]any{1: "x", 4: "y"}, + map[uint64]any{5: "z"}, "{1: x, 4: y, 5: z}", }, { - map[uint64]interface{}{1: "a", 4: "c"}, - map[uint64]interface{}{2: "b", 5: "d"}, + map[uint64]any{1: "a", 4: "c"}, + map[uint64]any{2: "b", 5: "d"}, "{1: a, 2: b, 4: c, 5: d}", }, { - map[uint64]interface{}{1: "a", 4: "c"}, - map[uint64]interface{}{2: "b", 5 + 8: "d"}, + map[uint64]any{1: "a", 4: "c"}, + map[uint64]any{2: "b", 5 + 8: "d"}, "{1: a, 2: b, 4: c, 13: d}", }, { - map[uint64]interface{}{2: "b", 5 + 8: "d"}, - map[uint64]interface{}{1: "a", 4: "c"}, + map[uint64]any{2: "b", 5 + 8: "d"}, + map[uint64]any{1: "a", 4: "c"}, "{1: a, 2: b, 4: c, 13: d}", }, { - map[uint64]interface{}{1: "a", 4: "c"}, - map[uint64]interface{}{2: "b", 5 + 8: "d"}, + map[uint64]any{1: "a", 4: "c"}, + map[uint64]any{2: "b", 5 + 8: "d"}, "{1: a, 2: b, 4: c, 13: d}", }, { - map[uint64]interface{}{2: "b", 5 + 8: "d"}, - map[uint64]interface{}{1: "a", 4: "c"}, + map[uint64]any{2: "b", 5 + 8: "d"}, + map[uint64]any{1: "a", 4: "c"}, "{1: a, 2: b, 4: c, 13: d}", }, { - map[uint64]interface{}{2: "b", 5 + 8: "d"}, - map[uint64]interface{}{2: "", 3: "a"}, + map[uint64]any{2: "b", 5 + 8: "d"}, + map[uint64]any{2: "", 3: "a"}, "{2: b, 3: a, 13: d}", }, { // crafted for `!prefixesOverlap(p, m, q, n)` - left: map[uint64]interface{}{1: "a", 2 + 1: "b"}, - right: map[uint64]interface{}{4 + 1: "c", 4 + 2: "d"}, + left: map[uint64]any{1: "a", 2 + 1: "b"}, + right: map[uint64]any{4 + 1: "c", 4 + 2: "d"}, // p: 5, m: 2 q: 1, n: 2 want: "{1: a, 3: b, 5: c, 6: d}", }, { // crafted for `ord(m, n) && !zeroBit(q, m)` - left: map[uint64]interface{}{8 + 2 + 1: "a", 16 + 4: "b"}, - right: map[uint64]interface{}{16 + 8 + 2 + 1: "c", 16 + 8 + 4 + 2 + 1: "d"}, + left: map[uint64]any{8 + 2 + 1: "a", 16 + 4: "b"}, + right: map[uint64]any{16 + 8 + 2 + 1: "c", 16 + 8 + 4 + 2 + 1: "d"}, // left: p: 15, m: 16 // right: q: 27, n: 4 want: "{11: a, 20: b, 27: c, 31: d}", @@ -347,8 +347,8 @@ func TestMerge(t *testing.T) { { // crafted for `ord(n, m) && !zeroBit(p, n)` // p: 6, m: 1 q: 5, n: 2 - left: map[uint64]interface{}{4 + 2: "b", 4 + 2 + 1: "c"}, - right: map[uint64]interface{}{4: "a", 4 + 2 + 1: "dropped"}, + left: map[uint64]any{4 + 2: "b", 4 + 2 + 1: "c"}, + right: map[uint64]any{4: "a", 4 + 2 + 1: "dropped"}, want: "{4: a, 6: b, 7: c}", }, } { @@ -364,65 +364,65 @@ func TestIntersect(t *testing.T) { // Most of the test cases go after specific branches of intersect. b := NewBuilder() for _, c := range []struct { - left map[uint64]interface{} - right map[uint64]interface{} + left map[uint64]any + right map[uint64]any want string }{ { - left: map[uint64]interface{}{10: "a", 39: "b"}, - right: map[uint64]interface{}{10: "A", 39: "B", 75: "C"}, + left: map[uint64]any{10: "a", 39: "b"}, + right: map[uint64]any{10: "A", 39: "B", 75: "C"}, want: "{10: a, 39: b}", }, { - left: map[uint64]interface{}{10: "a", 39: "b"}, - right: map[uint64]interface{}{}, + left: map[uint64]any{10: "a", 39: "b"}, + right: map[uint64]any{}, want: "{}", }, { - left: map[uint64]interface{}{}, - right: map[uint64]interface{}{10: "A", 39: "B", 75: "C"}, + left: map[uint64]any{}, + right: map[uint64]any{10: "A", 39: "B", 75: "C"}, want: "{}", }, { // m == n && p == q && left.(*empty) case - left: map[uint64]interface{}{4: 1, 6: 3, 10: 8, 15: "on left"}, - right: map[uint64]interface{}{0: 8, 7: 6, 11: 0, 15: "on right"}, + left: map[uint64]any{4: 1, 6: 3, 10: 8, 15: "on left"}, + right: map[uint64]any{0: 8, 7: 6, 11: 0, 15: "on right"}, want: "{15: on left}", }, { // m == n && p == q && right.(*empty) case - left: map[uint64]interface{}{0: "on left", 1: 2, 2: 3, 3: 1, 7: 3}, - right: map[uint64]interface{}{0: "on right", 5: 1, 6: 8}, + left: map[uint64]any{0: "on left", 1: 2, 2: 3, 3: 1, 7: 3}, + right: map[uint64]any{0: "on right", 5: 1, 6: 8}, want: "{0: on left}", }, { // m == n && p == q && both left and right are not empty - left: map[uint64]interface{}{1: "a", 2: "b", 3: "c"}, - right: map[uint64]interface{}{0: "A", 1: "B", 2: "C"}, + left: map[uint64]any{1: "a", 2: "b", 3: "c"}, + right: map[uint64]any{0: "A", 1: "B", 2: "C"}, want: "{1: a, 2: b}", }, { // m == n && p == q && both left and right are not empty - left: map[uint64]interface{}{1: "a", 2: "b", 3: "c"}, - right: map[uint64]interface{}{0: "A", 1: "B", 2: "C"}, + left: map[uint64]any{1: "a", 2: "b", 3: "c"}, + right: map[uint64]any{0: "A", 1: "B", 2: "C"}, want: "{1: a, 2: b}", }, { // !prefixesOverlap(p, m, q, n) // p = 1, m = 2, q = 5, n = 2 - left: map[uint64]interface{}{0b001: 1, 0b011: 3}, - right: map[uint64]interface{}{0b100: 4, 0b111: 7}, + left: map[uint64]any{0b001: 1, 0b011: 3}, + right: map[uint64]any{0b100: 4, 0b111: 7}, want: "{}", }, { // ord(m, n) && zeroBit(q, m) // p = 3, m = 4, q = 0, n = 1 - left: map[uint64]interface{}{0b010: 2, 0b101: 5}, - right: map[uint64]interface{}{0b000: 0, 0b001: 1}, + left: map[uint64]any{0b010: 2, 0b101: 5}, + right: map[uint64]any{0b000: 0, 0b001: 1}, want: "{}", }, { // ord(m, n) && !zeroBit(q, m) // p = 29, m = 2, q = 30, n = 1 - left: map[uint64]interface{}{ + left: map[uint64]any{ 0b11101: "29", 0b11110: "30", }, - right: map[uint64]interface{}{ + right: map[uint64]any{ 0b11110: "30 on right", 0b11111: "31", }, @@ -430,14 +430,14 @@ func TestIntersect(t *testing.T) { }, { // ord(n, m) && zeroBit(p, n) // p = 5, m = 2, q = 3, n = 4 - left: map[uint64]interface{}{0b000: 0, 0b001: 1}, - right: map[uint64]interface{}{0b010: 2, 0b101: 5}, + left: map[uint64]any{0b000: 0, 0b001: 1}, + right: map[uint64]any{0b010: 2, 0b101: 5}, want: "{}", }, { // default case // p = 5, m = 2, q = 3, n = 4 - left: map[uint64]interface{}{0b100: 1, 0b110: 3}, - right: map[uint64]interface{}{0b000: 8, 0b111: 6}, + left: map[uint64]any{0b100: 1, 0b110: 3}, + right: map[uint64]any{0b000: 8, 0b111: 6}, want: "{}", }, } { @@ -451,10 +451,10 @@ func TestIntersect(t *testing.T) { func TestIntersectWith(t *testing.T) { b := NewBuilder() - l := b.Create(map[uint64]interface{}{10: 2.0, 39: 32.0}) - r := b.Create(map[uint64]interface{}{10: 6.0, 39: 10.0, 75: 1.0}) + l := b.Create(map[uint64]any{10: 2.0, 39: 32.0}) + r := b.Create(map[uint64]any{10: 6.0, 39: 10.0, 75: 1.0}) - prodIfDifferent := func(x interface{}, y interface{}) interface{} { + prodIfDifferent := func(x any, y any) any { if x, ok := x.(float64); ok { if y, ok := y.(float64); ok { if x == y { @@ -478,24 +478,24 @@ func TestRemove(t *testing.T) { // Most of the test cases go after specific branches of intersect. b := NewBuilder() for _, c := range []struct { - m map[uint64]interface{} + m map[uint64]any key uint64 want string }{ - {map[uint64]interface{}{}, 10, "{}"}, - {map[uint64]interface{}{10: "a"}, 10, "{}"}, - {map[uint64]interface{}{39: "b"}, 10, "{39: b}"}, + {map[uint64]any{}, 10, "{}"}, + {map[uint64]any{10: "a"}, 10, "{}"}, + {map[uint64]any{39: "b"}, 10, "{39: b}"}, // Branch cases: // !matchPrefix(kp, br.prefix, br.branching) - {map[uint64]interface{}{10: "a", 39: "b"}, 128, "{10: a, 39: b}"}, + {map[uint64]any{10: "a", 39: "b"}, 128, "{10: a, 39: b}"}, // case: left == br.left && right == br.right - {map[uint64]interface{}{10: "a", 39: "b"}, 16, "{10: a, 39: b}"}, + {map[uint64]any{10: "a", 39: "b"}, 16, "{10: a, 39: b}"}, // left updated and is empty. - {map[uint64]interface{}{10: "a", 39: "b"}, 10, "{39: b}"}, + {map[uint64]any{10: "a", 39: "b"}, 10, "{39: b}"}, // right updated and is empty. - {map[uint64]interface{}{10: "a", 39: "b"}, 39, "{10: a}"}, + {map[uint64]any{10: "a", 39: "b"}, 39, "{10: a}"}, // final b.mkBranch(...) case. - {map[uint64]interface{}{10: "a", 39: "b", 128: "c"}, 39, "{10: a, 128: c}"}, + {map[uint64]any{10: "a", 39: "b", 128: "c"}, 39, "{10: a, 128: c}"}, } { pre := b.Create(c.m) post := b.Remove(pre, c.key) @@ -507,8 +507,8 @@ func TestRemove(t *testing.T) { func TestRescope(t *testing.T) { b := NewBuilder() - l := b.Create(map[uint64]interface{}{10: "a", 39: "b"}) - r := b.Create(map[uint64]interface{}{10: "A", 39: "B", 75: "C"}) + l := b.Create(map[uint64]any{10: "a", 39: "b"}) + r := b.Create(map[uint64]any{10: "A", 39: "B", 75: "C"}) b.Rescope() @@ -526,8 +526,8 @@ func TestRescope(t *testing.T) { func TestSharing(t *testing.T) { b := NewBuilder() - l := b.Create(map[uint64]interface{}{0: "a", 1: "b"}) - r := b.Create(map[uint64]interface{}{1: "B", 2: "C"}) + l := b.Create(map[uint64]any{0: "a", 1: "b"}) + r := b.Create(map[uint64]any{1: "B", 2: "C"}) rleftold := r.n.(*branch).left diff --git a/go/callgraph/vta/propagation.go b/go/callgraph/vta/propagation.go index f448cde1135..6ce1ca9e322 100644 --- a/go/callgraph/vta/propagation.go +++ b/go/callgraph/vta/propagation.go @@ -120,7 +120,7 @@ func (ptm propTypeMap) propTypes(n node) func(yield func(propType) bool) { // (https://go.dev/issue/65237). return func(yield func(propType) bool) { if types := ptm[n]; types != nil { - types.M.Range(func(_ uint64, elem interface{}) bool { + types.M.Range(func(_ uint64, elem any) bool { return yield(elem.(propType)) }) } diff --git a/go/callgraph/vta/vta_test.go b/go/callgraph/vta/vta_test.go index ea7d584d2d9..42610abb139 100644 --- a/go/callgraph/vta/vta_test.go +++ b/go/callgraph/vta/vta_test.go @@ -118,7 +118,7 @@ func TestVTAProgVsFuncSet(t *testing.T) { // available, which can happen when using analysis package. A successful // test simply does not panic. func TestVTAPanicMissingDefinitions(t *testing.T) { - run := func(pass *analysis.Pass) (interface{}, error) { + run := func(pass *analysis.Pass) (any, error) { s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) CallGraph(ssautil.AllFunctions(s.Pkg.Prog), cha.CallGraph(s.Pkg.Prog)) return nil, nil diff --git a/go/expect/expect.go b/go/expect/expect.go index be0e1dd23e6..1c002d91b60 100644 --- a/go/expect/expect.go +++ b/go/expect/expect.go @@ -66,9 +66,9 @@ import ( // It knows the position of the start of the comment, and the name and // arguments that make up the note. type Note struct { - Pos token.Pos // The position at which the note identifier appears - Name string // the name associated with the note - Args []interface{} // the arguments for the note + Pos token.Pos // The position at which the note identifier appears + Name string // the name associated with the note + Args []any // the arguments for the note } // ReadFile is the type of a function that can provide file contents for a @@ -85,7 +85,7 @@ type ReadFile func(filename string) ([]byte, error) // MatchBefore returns the range of the line that matched the pattern, and // invalid positions if there was no match, or an error if the line could not be // found. -func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern interface{}) (token.Pos, token.Pos, error) { +func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern any) (token.Pos, token.Pos, error) { f := fset.File(end) content, err := readFile(f.Name()) if err != nil { diff --git a/go/expect/expect_test.go b/go/expect/expect_test.go index cc585418d1b..d1ce96b868e 100644 --- a/go/expect/expect_test.go +++ b/go/expect/expect_test.go @@ -18,7 +18,7 @@ func TestMarker(t *testing.T) { filename string expectNotes int expectMarkers map[string]string - expectChecks map[string][]interface{} + expectChecks map[string][]any }{ { filename: "testdata/test.go", @@ -36,7 +36,7 @@ func TestMarker(t *testing.T) { "NonIdentifier": "+", "StringMarker": "\"hello\"", }, - expectChecks: map[string][]interface{}{ + expectChecks: map[string][]any{ "αSimpleMarker": nil, "StringAndInt": {"Number %d", int64(12)}, "Bool": {true}, @@ -140,7 +140,7 @@ func TestMarker(t *testing.T) { } } -func checkMarker(t *testing.T, fset *token.FileSet, readFile expect.ReadFile, markers map[string]token.Pos, pos token.Pos, name string, pattern interface{}) { +func checkMarker(t *testing.T, fset *token.FileSet, readFile expect.ReadFile, markers map[string]token.Pos, pos token.Pos, name string, pattern any) { start, end, err := expect.MatchBefore(fset, readFile, pos, pattern) if err != nil { t.Errorf("%v: MatchBefore failed: %v", fset.Position(pos), err) diff --git a/go/expect/extract.go b/go/expect/extract.go index 902b1e806e4..9cc5c8171fd 100644 --- a/go/expect/extract.go +++ b/go/expect/extract.go @@ -32,7 +32,7 @@ type Identifier string // See the package documentation for details about the syntax of those // notes. func Parse(fset *token.FileSet, filename string, content []byte) ([]*Note, error) { - var src interface{} + var src any if content != nil { src = content } @@ -220,7 +220,7 @@ func (t *tokens) Pos() token.Pos { return t.base + token.Pos(t.scanner.Position.Offset) } -func (t *tokens) Errorf(msg string, args ...interface{}) { +func (t *tokens) Errorf(msg string, args ...any) { if t.err != nil { return } @@ -280,9 +280,9 @@ func parseNote(t *tokens) *Note { } } -func parseArgumentList(t *tokens) []interface{} { - args := []interface{}{} // @name() is represented by a non-nil empty slice. - t.Consume() // '(' +func parseArgumentList(t *tokens) []any { + args := []any{} // @name() is represented by a non-nil empty slice. + t.Consume() // '(' t.Skip('\n') for t.Token() != ')' { args = append(args, parseArgument(t)) @@ -300,7 +300,7 @@ func parseArgumentList(t *tokens) []interface{} { return args } -func parseArgument(t *tokens) interface{} { +func parseArgument(t *tokens) any { switch t.Token() { case scanner.Ident: v := t.Consume() diff --git a/go/internal/cgo/cgo.go b/go/internal/cgo/cgo.go index 697974bb9b2..735efeb531d 100644 --- a/go/internal/cgo/cgo.go +++ b/go/internal/cgo/cgo.go @@ -203,7 +203,7 @@ func envList(key, def string) []string { // stringList's arguments should be a sequence of string or []string values. // stringList flattens them into a single []string. -func stringList(args ...interface{}) []string { +func stringList(args ...any) []string { var x []string for _, arg := range args { switch arg := arg.(type) { diff --git a/go/internal/gccgoimporter/parser.go b/go/internal/gccgoimporter/parser.go index f70946edbe4..7b0702892c4 100644 --- a/go/internal/gccgoimporter/parser.go +++ b/go/internal/gccgoimporter/parser.go @@ -86,7 +86,7 @@ func (e importError) Error() string { return fmt.Sprintf("import error %s (byte offset = %d): %s", e.pos, e.pos.Offset, e.err) } -func (p *parser) error(err interface{}) { +func (p *parser) error(err any) { if s, ok := err.(string); ok { err = errors.New(s) } @@ -94,7 +94,7 @@ func (p *parser) error(err interface{}) { panic(importError{p.scanner.Pos(), err.(error)}) } -func (p *parser) errorf(format string, args ...interface{}) { +func (p *parser) errorf(format string, args ...any) { p.error(fmt.Errorf(format, args...)) } @@ -492,7 +492,7 @@ func (p *parser) reserve(n int) { // used to resolve named types, or it can be a *types.Pointer, // used to resolve pointers to named types in case they are referenced // by embedded fields. -func (p *parser) update(t types.Type, nlist []interface{}) { +func (p *parser) update(t types.Type, nlist []any) { if t == reserved { p.errorf("internal error: update(%v) invoked on reserved", nlist) } @@ -529,7 +529,7 @@ func (p *parser) update(t types.Type, nlist []interface{}) { // NamedType = TypeName [ "=" ] Type { Method } . // TypeName = ExportedName . // Method = "func" "(" Param ")" Name ParamList ResultList [InlineBody] ";" . -func (p *parser) parseNamedType(nlist []interface{}) types.Type { +func (p *parser) parseNamedType(nlist []any) types.Type { pkg, name := p.parseExportedName() scope := pkg.Scope() obj := scope.Lookup(name) @@ -648,7 +648,7 @@ func (p *parser) parseInt() int { // parseArrayOrSliceType parses an ArrayOrSliceType: // // ArrayOrSliceType = "[" [ int ] "]" Type . -func (p *parser) parseArrayOrSliceType(pkg *types.Package, nlist []interface{}) types.Type { +func (p *parser) parseArrayOrSliceType(pkg *types.Package, nlist []any) types.Type { p.expect('[') if p.tok == ']' { p.next() @@ -673,7 +673,7 @@ func (p *parser) parseArrayOrSliceType(pkg *types.Package, nlist []interface{}) // parseMapType parses a MapType: // // MapType = "map" "[" Type "]" Type . -func (p *parser) parseMapType(pkg *types.Package, nlist []interface{}) types.Type { +func (p *parser) parseMapType(pkg *types.Package, nlist []any) types.Type { p.expectKeyword("map") t := new(types.Map) @@ -691,7 +691,7 @@ func (p *parser) parseMapType(pkg *types.Package, nlist []interface{}) types.Typ // parseChanType parses a ChanType: // // ChanType = "chan" ["<-" | "-<"] Type . -func (p *parser) parseChanType(pkg *types.Package, nlist []interface{}) types.Type { +func (p *parser) parseChanType(pkg *types.Package, nlist []any) types.Type { p.expectKeyword("chan") t := new(types.Chan) @@ -720,7 +720,7 @@ func (p *parser) parseChanType(pkg *types.Package, nlist []interface{}) types.Ty // parseStructType parses a StructType: // // StructType = "struct" "{" { Field } "}" . -func (p *parser) parseStructType(pkg *types.Package, nlist []interface{}) types.Type { +func (p *parser) parseStructType(pkg *types.Package, nlist []any) types.Type { p.expectKeyword("struct") t := new(types.Struct) @@ -793,7 +793,7 @@ func (p *parser) parseResultList(pkg *types.Package) *types.Tuple { // parseFunctionType parses a FunctionType: // // FunctionType = ParamList ResultList . -func (p *parser) parseFunctionType(pkg *types.Package, nlist []interface{}) *types.Signature { +func (p *parser) parseFunctionType(pkg *types.Package, nlist []any) *types.Signature { t := new(types.Signature) p.update(t, nlist) @@ -837,7 +837,7 @@ func (p *parser) parseFunc(pkg *types.Package) *types.Func { // parseInterfaceType parses an InterfaceType: // // InterfaceType = "interface" "{" { ("?" Type | Func) ";" } "}" . -func (p *parser) parseInterfaceType(pkg *types.Package, nlist []interface{}) types.Type { +func (p *parser) parseInterfaceType(pkg *types.Package, nlist []any) types.Type { p.expectKeyword("interface") t := new(types.Interface) @@ -868,7 +868,7 @@ func (p *parser) parseInterfaceType(pkg *types.Package, nlist []interface{}) typ // parsePointerType parses a PointerType: // // PointerType = "*" ("any" | Type) . -func (p *parser) parsePointerType(pkg *types.Package, nlist []interface{}) types.Type { +func (p *parser) parsePointerType(pkg *types.Package, nlist []any) types.Type { p.expect('*') if p.tok == scanner.Ident { p.expectKeyword("any") @@ -888,7 +888,7 @@ func (p *parser) parsePointerType(pkg *types.Package, nlist []interface{}) types // parseTypeSpec parses a TypeSpec: // // TypeSpec = NamedType | MapType | ChanType | StructType | InterfaceType | PointerType | ArrayOrSliceType | FunctionType . -func (p *parser) parseTypeSpec(pkg *types.Package, nlist []interface{}) types.Type { +func (p *parser) parseTypeSpec(pkg *types.Package, nlist []any) types.Type { switch p.tok { case scanner.String: return p.parseNamedType(nlist) @@ -980,14 +980,14 @@ func lookupBuiltinType(typ int) types.Type { // Type = "<" "type" ( "-" int | int [ TypeSpec ] ) ">" . // // parseType updates the type map to t for all type numbers n. -func (p *parser) parseType(pkg *types.Package, n ...interface{}) types.Type { +func (p *parser) parseType(pkg *types.Package, n ...any) types.Type { p.expect('<') t, _ := p.parseTypeAfterAngle(pkg, n...) return t } // (*parser).Type after reading the "<". -func (p *parser) parseTypeAfterAngle(pkg *types.Package, n ...interface{}) (t types.Type, n1 int) { +func (p *parser) parseTypeAfterAngle(pkg *types.Package, n ...any) (t types.Type, n1 int) { p.expectKeyword("type") n1 = 0 @@ -1030,7 +1030,7 @@ func (p *parser) parseTypeAfterAngle(pkg *types.Package, n ...interface{}) (t ty // parseTypeExtended is identical to parseType, but if the type in // question is a saved type, returns the index as well as the type // pointer (index returned is zero if we parsed a builtin). -func (p *parser) parseTypeExtended(pkg *types.Package, n ...interface{}) (t types.Type, n1 int) { +func (p *parser) parseTypeExtended(pkg *types.Package, n ...any) (t types.Type, n1 int) { p.expect('<') t, n1 = p.parseTypeAfterAngle(pkg, n...) return @@ -1119,7 +1119,7 @@ func (p *parser) parseTypes(pkg *types.Package) { } // parseSavedType parses one saved type definition. -func (p *parser) parseSavedType(pkg *types.Package, i int, nlist []interface{}) { +func (p *parser) parseSavedType(pkg *types.Package, i int, nlist []any) { defer func(s *scanner.Scanner, tok rune, lit string) { p.scanner = s p.tok = tok diff --git a/go/loader/loader.go b/go/loader/loader.go index 2d4865f664f..d06f95ad76c 100644 --- a/go/loader/loader.go +++ b/go/loader/loader.go @@ -215,7 +215,7 @@ func (conf *Config) fset() *token.FileSet { // src specifies the parser input as a string, []byte, or io.Reader, and // filename is its apparent name. If src is nil, the contents of // filename are read from the file system. -func (conf *Config) ParseFile(filename string, src interface{}) (*ast.File, error) { +func (conf *Config) ParseFile(filename string, src any) (*ast.File, error) { // TODO(adonovan): use conf.build() etc like parseFiles does. return parser.ParseFile(conf.fset(), filename, src, conf.ParserMode) } diff --git a/go/packages/gopackages/main.go b/go/packages/gopackages/main.go index 3841ac3410b..7ec0bdc7bdd 100644 --- a/go/packages/gopackages/main.go +++ b/go/packages/gopackages/main.go @@ -248,7 +248,7 @@ func (app *application) print(lpkg *packages.Package) { // e.g. --flag=one --flag=two would produce []string{"one", "two"}. type stringListValue []string -func (ss *stringListValue) Get() interface{} { return []string(*ss) } +func (ss *stringListValue) Get() any { return []string(*ss) } func (ss *stringListValue) String() string { return fmt.Sprintf("%q", *ss) } diff --git a/go/packages/overlay_test.go b/go/packages/overlay_test.go index 9edd0d646ed..1108461926f 100644 --- a/go/packages/overlay_test.go +++ b/go/packages/overlay_test.go @@ -32,7 +32,7 @@ func testOverlayChangesPackageName(t *testing.T, exporter packagestest.Exporter) log.SetFlags(log.Lshortfile) exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a.go": "package foo\nfunc f(){}\n", }, Overlay: map[string][]byte{ @@ -62,7 +62,7 @@ func testOverlayChangesBothPackageNames(t *testing.T, exporter packagestest.Expo log.SetFlags(log.Lshortfile) exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a.go": "package foo\nfunc g(){}\n", "a_test.go": "package foo\nfunc f(){}\n", }, @@ -110,7 +110,7 @@ func TestOverlayChangesTestPackageName(t *testing.T) { func testOverlayChangesTestPackageName(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a_test.go": "package foo\nfunc f(){}\n", }, Overlay: map[string][]byte{ @@ -194,7 +194,7 @@ func TestHello(t *testing.T) { // First, get the source of truth by loading the package, all on disk. onDisk := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": aFile, "a/a_test.go": aTestVariant, "a/a_x_test.go": aXTest, @@ -213,7 +213,7 @@ func TestHello(t *testing.T) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": aFile, "a/a_test.go": aTestVariant, "a/a_x_test.go": ``, // empty x test on disk @@ -248,7 +248,7 @@ func TestOverlay(t *testing.T) { testAllOrModulesParallel(t, testOverlay) } func testOverlay(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "golang.org/fake/b"; const A = "a" + b.B`, "b/b.go": `package b; import "golang.org/fake/c"; const B = "b" + c.C`, "c/c.go": `package c; const C = "c"`, @@ -316,7 +316,7 @@ func TestOverlayDeps(t *testing.T) { testAllOrModulesParallel(t, testOverlayDeps func testOverlayDeps(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "c/c.go": `package c; const C = "c"`, "c/c_test.go": `package c; import "testing"; func TestC(t *testing.T) {}`, }, @@ -366,7 +366,7 @@ func testNewPackagesInOverlay(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "golang.org/fake/b"; const A = "a" + b.B`, "b/b.go": `package b; import "golang.org/fake/c"; const B = "b" + c.C`, "c/c.go": `package c; const C = "c"`, @@ -375,7 +375,7 @@ func testNewPackagesInOverlay(t *testing.T, exporter packagestest.Exporter) { }, { Name: "example.com/extramodule", - Files: map[string]interface{}{ + Files: map[string]any{ "pkg/x.go": "package pkg\n", }, }, @@ -471,7 +471,7 @@ func testOverlayNewPackageAndTest(t *testing.T, exporter packagestest.Exporter) exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "foo.txt": "placeholder", }, }, @@ -623,7 +623,7 @@ func TestOverlayGOPATHVendoring(t *testing.T) { exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "vendor/vendor.com/foo/foo.go": `package foo; const X = "hi"`, "user/user.go": `package user`, }, @@ -652,7 +652,7 @@ func TestContainsOverlay(t *testing.T) { testAllOrModulesParallel(t, testContain func testContainsOverlay(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "golang.org/fake/b"`, "b/b.go": `package b; import "golang.org/fake/c"`, "c/c.go": `package c`, @@ -681,7 +681,7 @@ func TestContainsOverlayXTest(t *testing.T) { testAllOrModulesParallel(t, testCo func testContainsOverlayXTest(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "golang.org/fake/b"`, "b/b.go": `package b; import "golang.org/fake/c"`, "c/c.go": `package c`, @@ -717,7 +717,7 @@ func testInvalidFilesBeforeOverlay(t *testing.T, exporter packagestest.Exporter) exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "d/d.go": ``, "main.go": ``, }, @@ -754,7 +754,7 @@ func testInvalidFilesBeforeOverlayContains(t *testing.T, exporter packagestest.E exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "d/d.go": `package d; import "net/http"; const Get = http.MethodGet; const Hello = "hello";`, "d/util.go": ``, "d/d_test.go": ``, @@ -861,7 +861,7 @@ func testInvalidXTestInGOPATH(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "x/x.go": `package x`, "x/x_test.go": ``, }, @@ -892,7 +892,7 @@ func testAddImportInOverlay(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a import ( @@ -961,7 +961,7 @@ func testLoadDifferentPatterns(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "foo.txt": "placeholder", "b/b.go": `package b import "golang.org/fake/a" diff --git a/go/packages/packages.go b/go/packages/packages.go index 342f019a0f9..6665a04c173 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -163,7 +163,7 @@ type Config struct { // If the user provides a logger, debug logging is enabled. // If the GOPACKAGESDEBUG environment variable is set to true, // but the logger is nil, default to log.Printf. - Logf func(format string, args ...interface{}) + Logf func(format string, args ...any) // Dir is the directory in which to run the build system's query tool // that provides information about the packages. @@ -566,13 +566,13 @@ type ModuleError struct { } func init() { - packagesinternal.GetDepsErrors = func(p interface{}) []*packagesinternal.PackageError { + packagesinternal.GetDepsErrors = func(p any) []*packagesinternal.PackageError { return p.(*Package).depsErrors } - packagesinternal.SetModFile = func(config interface{}, value string) { + packagesinternal.SetModFile = func(config any, value string) { config.(*Config).modFile = value } - packagesinternal.SetModFlag = func(config interface{}, value string) { + packagesinternal.SetModFlag = func(config any, value string) { config.(*Config).modFlag = value } packagesinternal.TypecheckCgo = int(typecheckCgo) @@ -741,7 +741,7 @@ func newLoader(cfg *Config) *loader { if debug { ld.Config.Logf = log.Printf } else { - ld.Config.Logf = func(format string, args ...interface{}) {} + ld.Config.Logf = func(format string, args ...any) {} } } if ld.Config.Mode == 0 { diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 06fa488d1ed..5678b265561 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -129,7 +129,7 @@ func TestLoadImportsGraph(t *testing.T) { testAllOrModulesParallel(t, testLoadIm func testLoadImportsGraph(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; const A = 1`, "b/b.go": `package b; import ("golang.org/fake/a"; _ "container/list"); var B = a.A`, "c/c.go": `package c; import (_ "golang.org/fake/b"; _ "unsafe")`, @@ -305,7 +305,7 @@ func TestLoadImportsTestVariants(t *testing.T) { func testLoadImportsTestVariants(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import _ "golang.org/fake/b"`, "b/b.go": `package b`, "b/b_test.go": `package b`, @@ -346,11 +346,11 @@ func TestLoadAbsolutePath(t *testing.T) { exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{ Name: "golang.org/gopatha", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a`, }}, { Name: "golang.org/gopathb", - Files: map[string]interface{}{ + Files: map[string]any{ "b/b.go": `package b`, }}}) defer exported.Cleanup() @@ -381,7 +381,7 @@ func TestLoadArgumentListIsNotTooLong(t *testing.T) { argMax := 1_000_000 exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{ Name: "golang.org/mod", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": `package main"`, }}}) defer exported.Cleanup() @@ -402,7 +402,7 @@ func TestVendorImports(t *testing.T) { exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import _ "b"; import _ "golang.org/fake/c";`, "a/vendor/b/b.go": `package b; import _ "golang.org/fake/c"`, "c/c.go": `package c; import _ "b"`, @@ -463,7 +463,7 @@ func TestConfigDir(t *testing.T) { testAllOrModulesParallel(t, testConfigDir) } func testConfigDir(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; const Name = "a" `, "a/b/b.go": `package b; const Name = "a/b"`, "b/b.go": `package b; const Name = "b"`, @@ -522,7 +522,7 @@ func testConfigFlags(t *testing.T, exporter packagestest.Exporter) { // Test satisfying +build line tags, with -tags flag. exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ // package a "a/a.go": `package a; import _ "golang.org/fake/a/b"`, "a/b.go": `// +build tag @@ -587,7 +587,7 @@ func testLoadTypes(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "golang.org/fake/b"; import "golang.org/fake/c"; const A = "a" + b.B + c.C`, "b/b.go": `package b; const B = "b"`, "c/c.go": `package c; const C = "c" + 1`, @@ -640,7 +640,7 @@ func TestLoadTypesBits(t *testing.T) { testAllOrModulesParallel(t, testLoadTypes func testLoadTypesBits(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "golang.org/fake/b"; const A = "a" + b.B`, "b/b.go": `package b; import "golang.org/fake/c"; const B = "b" + c.C`, "c/c.go": `package c; import "golang.org/fake/d"; const C = "c" + d.D`, @@ -716,7 +716,7 @@ func TestLoadSyntaxOK(t *testing.T) { testAllOrModulesParallel(t, testLoadSyntax func testLoadSyntaxOK(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "golang.org/fake/b"; const A = "a" + b.B`, "b/b.go": `package b; import "golang.org/fake/c"; const B = "b" + c.C`, "c/c.go": `package c; import "golang.org/fake/d"; const C = "c" + d.D`, @@ -807,7 +807,7 @@ func testLoadDiamondTypes(t *testing.T, exporter packagestest.Exporter) { // We make a diamond dependency and check the type d.D is the same through both paths exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import ("golang.org/fake/b"; "golang.org/fake/c"); var _ = b.B == c.C`, "b/b.go": `package b; import "golang.org/fake/d"; var B d.D`, "c/c.go": `package c; import "golang.org/fake/d"; var C d.D`, @@ -850,7 +850,7 @@ func testLoadSyntaxError(t *testing.T, exporter packagestest.Exporter) { // should be IllTyped. exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "golang.org/fake/b"; const A = "a" + b.B`, "b/b.go": `package b; import "golang.org/fake/c"; const B = "b" + c.C`, "c/c.go": `package c; import "golang.org/fake/d"; const C = "c" + d.D`, @@ -922,7 +922,7 @@ func TestParseFileModifyAST(t *testing.T) { testAllOrModulesParallel(t, testPars func testParseFileModifyAST(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; const A = "a" `, }}}) defer exported.Cleanup() @@ -1010,7 +1010,7 @@ func testLoadAllSyntaxImportErrors(t *testing.T, exporter packagestest.Exporter) exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "unicycle/unicycle.go": `package unicycle; import _ "unicycle"`, "bicycle1/bicycle1.go": `package bicycle1; import _ "bicycle2"`, "bicycle2/bicycle2.go": `package bicycle2; import _ "bicycle1"`, @@ -1090,7 +1090,7 @@ func TestAbsoluteFilenames(t *testing.T) { testAllOrModulesParallel(t, testAbsol func testAbsoluteFilenames(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; const A = 1`, "b/b.go": `package b; import ("golang.org/fake/a"; _ "errors"); var B = a.A`, "b/vendor/a/a.go": `package a; const A = 1`, @@ -1180,7 +1180,7 @@ func TestContains(t *testing.T) { testAllOrModulesParallel(t, testContains) } func testContains(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "golang.org/fake/b"`, "b/b.go": `package b; import "golang.org/fake/c"`, "c/c.go": `package c`, @@ -1219,7 +1219,7 @@ func testSizes(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "unsafe"; const WordSize = 8*unsafe.Sizeof(int(0))`, }}}) defer exported.Cleanup() @@ -1257,7 +1257,7 @@ func TestNeedTypeSizesWithBadGOARCH(t *testing.T) { testAllOrModulesParallel(t, func(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "testdata", - Files: map[string]interface{}{"a/a.go": `package a`}}}) + Files: map[string]any{"a/a.go": `package a`}}}) defer exported.Cleanup() exported.Config.Mode = packages.NeedTypesSizes // or {,Info,Sizes} @@ -1280,7 +1280,7 @@ func TestContainsFallbackSticks(t *testing.T) { func testContainsFallbackSticks(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import "golang.org/fake/b"`, "b/b.go": `package b; import "golang.org/fake/c"`, "c/c.go": `package c`, @@ -1313,7 +1313,7 @@ func TestNoPatterns(t *testing.T) { testAllOrModulesParallel(t, testNoPatterns) func testNoPatterns(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a;`, "a/b/b.go": `package b;`, }}}) @@ -1336,7 +1336,7 @@ func testJSON(t *testing.T, exporter packagestest.Exporter) { // TODO: add in some errors exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; const A = 1`, "b/b.go": `package b; import "golang.org/fake/a"; var B = a.A`, "c/c.go": `package c; import "golang.org/fake/b" ; var C = b.B`, @@ -1503,7 +1503,7 @@ func TestPatternPassthrough(t *testing.T) { testAllOrModulesParallel(t, testPatt func testPatternPassthrough(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a;`, }}}) defer exported.Cleanup() @@ -1563,7 +1563,7 @@ EOF } exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "bin/gopackagesdriver": driverScript, "golist/golist.go": "package golist", }}}) @@ -1639,7 +1639,7 @@ func TestBasicXTest(t *testing.T) { testAllOrModulesParallel(t, testBasicXTest) func testBasicXTest(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a;`, "a/a_test.go": `package a_test;`, }}}) @@ -1657,7 +1657,7 @@ func TestErrorMissingFile(t *testing.T) { testAllOrModulesParallel(t, testErrorM func testErrorMissingFile(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a_test.go": `package a;`, }}}) defer exported.Cleanup() @@ -1685,11 +1685,11 @@ func TestReturnErrorWhenUsingNonGoFiles(t *testing.T) { func testReturnErrorWhenUsingNonGoFiles(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/gopatha", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a`, }}, { Name: "golang.org/gopathb", - Files: map[string]interface{}{ + Files: map[string]any{ "b/b.c": `package b`, }}}) defer exported.Cleanup() @@ -1713,7 +1713,7 @@ func TestReturnErrorWhenUsingGoFilesInMultipleDirectories(t *testing.T) { func testReturnErrorWhenUsingGoFilesInMultipleDirectories(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/gopatha", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a`, "b/b.go": `package b`, }}}) @@ -1745,7 +1745,7 @@ func TestReturnErrorForUnexpectedDirectoryLayout(t *testing.T) { func testReturnErrorForUnexpectedDirectoryLayout(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/gopatha", - Files: map[string]interface{}{ + Files: map[string]any{ "a/testdata/a.go": `package a; import _ "b"`, "a/vendor/b/b.go": `package b; import _ "fmt"`, }}}) @@ -1774,7 +1774,7 @@ func TestMissingDependency(t *testing.T) { testAllOrModulesParallel(t, testMissi func testMissingDependency(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import _ "this/package/doesnt/exist"`, }}}) defer exported.Cleanup() @@ -1796,7 +1796,7 @@ func TestAdHocContains(t *testing.T) { testAllOrModulesParallel(t, testAdHocCont func testAdHocContains(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a;`, }}}) defer exported.Cleanup() @@ -1839,7 +1839,7 @@ func testCgoNoCcompiler(t *testing.T, exporter packagestest.Exporter) { testenv.NeedsTool(t, "cgo") exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a import "net/http" const A = http.MethodGet @@ -1873,7 +1873,7 @@ func testCgoMissingFile(t *testing.T, exporter packagestest.Exporter) { testenv.NeedsTool(t, "cgo") exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a // #include "foo.h" @@ -1962,7 +1962,7 @@ func testCgoNoSyntax(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "c/c.go": `package c; import "C"`, }, }}) @@ -2005,7 +2005,7 @@ func testCgoBadPkgConfig(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "c/c.go": `package c // #cgo pkg-config: --cflags -- foo @@ -2074,7 +2074,7 @@ func TestIssue32814(t *testing.T) { testAllOrModulesParallel(t, testIssue32814) func testIssue32814(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{}}}) + Files: map[string]any{}}}) defer exported.Cleanup() exported.Config.Mode = packages.NeedName | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedTypesSizes @@ -2103,7 +2103,7 @@ func TestLoadTypesInfoWithoutNeedDeps(t *testing.T) { func testLoadTypesInfoWithoutNeedDeps(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import _ "golang.org/fake/b"`, "b/b.go": `package b`, }}}) @@ -2130,7 +2130,7 @@ func TestLoadWithNeedDeps(t *testing.T) { func testLoadWithNeedDeps(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import _ "golang.org/fake/b"`, "b/b.go": `package b; import _ "golang.org/fake/c"`, "c/c.go": `package c`, @@ -2174,7 +2174,7 @@ func TestImpliedLoadMode(t *testing.T) { func testImpliedLoadMode(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import _ "golang.org/fake/b"`, "b/b.go": `package b`, }}}) @@ -2243,7 +2243,7 @@ func TestMultiplePackageVersionsIssue36188(t *testing.T) { func testMultiplePackageVersionsIssue36188(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import _ "golang.org/fake/b"`, "b/b.go": `package main`, }}}) @@ -2363,7 +2363,7 @@ func TestCycleImportStack(t *testing.T) { func testCycleImportStack(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; import _ "golang.org/fake/b"`, "b/b.go": `package b; import _ "golang.org/fake/a"`, }}}) @@ -2393,7 +2393,7 @@ func TestForTestField(t *testing.T) { func testForTestField(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; func hello() {};`, "a/a_test.go": `package a; import "testing"; func TestA1(t *testing.T) {};`, "a/x_test.go": `package a_test; import "testing"; func TestA2(t *testing.T) {};`, @@ -2499,7 +2499,7 @@ func testIssue37098(t *testing.T, exporter packagestest.Exporter) { // file. exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ // The "package" statement must be included for SWIG sources to // be generated. "a/a.go": "package a", @@ -2550,7 +2550,7 @@ func TestIssue56632(t *testing.T) { exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{ Name: "golang.org/issue56632", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a`, "a/a_cgo.go": `package a @@ -2593,7 +2593,7 @@ func testInvalidFilesInXTest(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "d/d.go": `package d; import "net/http"; const d = http.MethodGet; func Get() string { return d; }`, "d/d2.go": ``, // invalid file "d/d_test.go": `package d_test; import "testing"; import "golang.org/fake/d"; func TestD(t *testing.T) { d.Get(); }`, @@ -2628,7 +2628,7 @@ func testTypecheckCgo(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "cgo/cgo.go": cgo, }, }, @@ -2662,7 +2662,7 @@ func testIssue48226(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake/syntax", - Files: map[string]interface{}{ + Files: map[string]any{ "syntax.go": `package test`, }, }, @@ -2697,7 +2697,7 @@ func TestModule(t *testing.T) { func testModule(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{"a/a.go": `package a`}}}) + Files: map[string]any{"a/a.go": `package a`}}}) exported.Config.Mode = packages.NeedModule rootDir := filepath.Dir(filepath.Dir(exported.File("golang.org/fake", "a/a.go"))) @@ -2746,7 +2746,7 @@ func testExternal_NotHandled(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a`, "empty_driver/main.go": `package main @@ -2825,7 +2825,7 @@ func TestInvalidPackageName(t *testing.T) { func testInvalidPackageName(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": `package default func main() { @@ -3206,7 +3206,7 @@ func TestLoadTypesInfoWithoutSyntaxOrTypes(t *testing.T) { func testLoadTypesInfoWithoutSyntaxOrTypes(t *testing.T, exporter packagestest.Exporter) { exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "a/a.go": `package a; func foo() int { diff --git a/go/packages/packagestest/expect.go b/go/packages/packagestest/expect.go index dc41894a6ed..4be34191e62 100644 --- a/go/packages/packagestest/expect.go +++ b/go/packages/packagestest/expect.go @@ -72,7 +72,7 @@ const ( // // It is safe to call this repeatedly with different method sets, but it is // not safe to call it concurrently. -func (e *Exported) Expect(methods map[string]interface{}) error { +func (e *Exported) Expect(methods map[string]any) error { if err := e.getNotes(); err != nil { return err } @@ -98,7 +98,7 @@ func (e *Exported) Expect(methods map[string]interface{}) error { n = &expect.Note{ Pos: n.Pos, Name: markMethod, - Args: []interface{}{n.Name, n.Name}, + Args: []any{n.Name, n.Name}, } } mi, ok := ms[n.Name] @@ -222,7 +222,7 @@ func (e *Exported) getMarkers() error { } // set markers early so that we don't call getMarkers again from Expect e.markers = make(map[string]Range) - return e.Expect(map[string]interface{}{ + return e.Expect(map[string]any{ markMethod: e.Mark, }) } @@ -243,7 +243,7 @@ var ( // It takes the args remaining, and returns the args it did not consume. // This allows a converter to consume 0 args for well known types, or multiple // args for compound types. -type converter func(*expect.Note, []interface{}) (reflect.Value, []interface{}, error) +type converter func(*expect.Note, []any) (reflect.Value, []any, error) // method is used to track information about Invoke methods that is expensive to // calculate so that we can work it out once rather than per marker. @@ -259,19 +259,19 @@ type method struct { func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { switch { case pt == noteType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { return reflect.ValueOf(n), args, nil }, nil case pt == fsetType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { return reflect.ValueOf(e.ExpectFileSet), args, nil }, nil case pt == exportedType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { return reflect.ValueOf(e), args, nil }, nil case pt == posType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err @@ -279,7 +279,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { return reflect.ValueOf(r.Start), remains, nil }, nil case pt == positionType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err @@ -287,7 +287,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { return reflect.ValueOf(e.ExpectFileSet.Position(r.Start)), remains, nil }, nil case pt == rangeType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err @@ -295,7 +295,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { return reflect.ValueOf(r), remains, nil }, nil case pt == identifierType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -310,7 +310,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { }, nil case pt == regexType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -323,7 +323,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { }, nil case pt.Kind() == reflect.String: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -339,7 +339,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } }, nil case pt.Kind() == reflect.Int64: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -353,7 +353,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } }, nil case pt.Kind() == reflect.Bool: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -366,7 +366,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { return reflect.ValueOf(b), args, nil }, nil case pt.Kind() == reflect.Slice: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { converter, err := e.buildConverter(pt.Elem()) if err != nil { return reflect.Value{}, nil, err @@ -384,7 +384,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { }, nil default: if pt.Kind() == reflect.Interface && pt.NumMethod() == 0 { - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -395,7 +395,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } } -func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (Range, []interface{}, error) { +func (e *Exported) rangeConverter(n *expect.Note, args []any) (Range, []any, error) { tokFile := e.ExpectFileSet.File(n.Pos) if len(args) < 1 { return Range{}, nil, fmt.Errorf("missing argument") diff --git a/go/packages/packagestest/expect_test.go b/go/packages/packagestest/expect_test.go index 46d96d61fb9..70ff6656012 100644 --- a/go/packages/packagestest/expect_test.go +++ b/go/packages/packagestest/expect_test.go @@ -19,7 +19,7 @@ func TestExpect(t *testing.T) { }}) defer exported.Cleanup() checkCount := 0 - if err := exported.Expect(map[string]interface{}{ + if err := exported.Expect(map[string]any{ "check": func(src, target token.Position) { checkCount++ }, diff --git a/go/packages/packagestest/export.go b/go/packages/packagestest/export.go index 47e6d11b94b..4ac4967b46b 100644 --- a/go/packages/packagestest/export.go +++ b/go/packages/packagestest/export.go @@ -101,7 +101,7 @@ type Module struct { // The keys are the file fragment that follows the module name, the value can // be a string or byte slice, in which case it is the contents of the // file, otherwise it must be a Writer function. - Files map[string]interface{} + Files map[string]any // Overlay is the set of source file overlays for the module. // The keys are the file fragment as in the Files configuration. @@ -483,7 +483,7 @@ func GroupFilesByModules(root string) ([]Module, error) { primarymod := &Module{ Name: root, - Files: make(map[string]interface{}), + Files: make(map[string]any), Overlay: make(map[string][]byte), } mods := map[string]*Module{ @@ -573,7 +573,7 @@ func GroupFilesByModules(root string) ([]Module, error) { } mods[path] = &Module{ Name: filepath.ToSlash(module), - Files: make(map[string]interface{}), + Files: make(map[string]any), Overlay: make(map[string][]byte), } currentModule = path @@ -591,8 +591,8 @@ func GroupFilesByModules(root string) ([]Module, error) { // This is to enable the common case in tests where you have a full copy of the // package in your testdata. // This will panic if there is any kind of error trying to walk the file tree. -func MustCopyFileTree(root string) map[string]interface{} { - result := map[string]interface{}{} +func MustCopyFileTree(root string) map[string]any { + result := map[string]any{} if err := filepath.Walk(filepath.FromSlash(root), func(path string, info os.FileInfo, err error) error { if err != nil { return err diff --git a/go/packages/packagestest/export_test.go b/go/packages/packagestest/export_test.go index eb13f560916..e3e4658efb6 100644 --- a/go/packages/packagestest/export_test.go +++ b/go/packages/packagestest/export_test.go @@ -16,7 +16,7 @@ import ( var testdata = []packagestest.Module{{ Name: "golang.org/fake1", - Files: map[string]interface{}{ + Files: map[string]any{ "a.go": packagestest.Symlink("testdata/a.go"), // broken symlink "b.go": "invalid file contents", }, @@ -26,22 +26,22 @@ var testdata = []packagestest.Module{{ }, }, { Name: "golang.org/fake2", - Files: map[string]interface{}{ + Files: map[string]any{ "other/a.go": "package fake2", }, }, { Name: "golang.org/fake2/v2", - Files: map[string]interface{}{ + Files: map[string]any{ "other/a.go": "package fake2", }, }, { Name: "golang.org/fake3@v1.0.0", - Files: map[string]interface{}{ + Files: map[string]any{ "other/a.go": "package fake3", }, }, { Name: "golang.org/fake3@v1.1.0", - Files: map[string]interface{}{ + Files: map[string]any{ "other/a.go": "package fake3", }, }} @@ -97,13 +97,13 @@ func TestGroupFilesByModules(t *testing.T) { want: []packagestest.Module{ { Name: "testdata/groups/one", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": true, }, }, { Name: "example.com/extra", - Files: map[string]interface{}{ + Files: map[string]any{ "help.go": true, }, }, @@ -114,7 +114,7 @@ func TestGroupFilesByModules(t *testing.T) { want: []packagestest.Module{ { Name: "testdata/groups/two", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": true, "expect/yo.go": true, "expect/yo_test.go": true, @@ -122,33 +122,33 @@ func TestGroupFilesByModules(t *testing.T) { }, { Name: "example.com/extra", - Files: map[string]interface{}{ + Files: map[string]any{ "yo.go": true, "geez/help.go": true, }, }, { Name: "example.com/extra/v2", - Files: map[string]interface{}{ + Files: map[string]any{ "me.go": true, "geez/help.go": true, }, }, { Name: "example.com/tempmod", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": true, }, }, { Name: "example.com/what@v1.0.0", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": true, }, }, { Name: "example.com/what@v1.1.0", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": true, }, }, diff --git a/go/ssa/const_test.go b/go/ssa/const_test.go index 6738f07b2ef..6097bd93757 100644 --- a/go/ssa/const_test.go +++ b/go/ssa/const_test.go @@ -39,9 +39,9 @@ func TestConstString(t *testing.T) { } for _, test := range []struct { - expr string // type expression - constant interface{} // constant value - want string // expected String() value + expr string // type expression + constant any // constant value + want string // expected String() value }{ {"int", int64(0), "0:int"}, {"int64", int64(0), "0:int64"}, diff --git a/go/ssa/interp/interp.go b/go/ssa/interp/interp.go index f80db0676c7..7bd06120f6c 100644 --- a/go/ssa/interp/interp.go +++ b/go/ssa/interp/interp.go @@ -109,7 +109,7 @@ type frame struct { defers *deferred result value panicking bool - panic interface{} + panic any phitemps []value // temporaries for parallel phi assignment } diff --git a/go/ssa/interp/map.go b/go/ssa/interp/map.go index f5d5f230b73..e96e44df2b9 100644 --- a/go/ssa/interp/map.go +++ b/go/ssa/interp/map.go @@ -17,7 +17,7 @@ import ( type hashable interface { hash(t types.Type) int - eq(t types.Type, x interface{}) bool + eq(t types.Type, x any) bool } type entry struct { diff --git a/go/ssa/interp/value.go b/go/ssa/interp/value.go index bd681cb6152..4d65aa6c83e 100644 --- a/go/ssa/interp/value.go +++ b/go/ssa/interp/value.go @@ -48,7 +48,7 @@ import ( "golang.org/x/tools/go/types/typeutil" ) -type value interface{} +type value any type tuple []value @@ -123,7 +123,7 @@ func usesBuiltinMap(t types.Type) bool { panic(fmt.Sprintf("invalid map key type: %T", t)) } -func (x array) eq(t types.Type, _y interface{}) bool { +func (x array) eq(t types.Type, _y any) bool { y := _y.(array) tElt := t.Underlying().(*types.Array).Elem() for i, xi := range x { @@ -143,7 +143,7 @@ func (x array) hash(t types.Type) int { return h } -func (x structure) eq(t types.Type, _y interface{}) bool { +func (x structure) eq(t types.Type, _y any) bool { y := _y.(structure) tStruct := t.Underlying().(*types.Struct) for i, n := 0, tStruct.NumFields(); i < n; i++ { @@ -175,7 +175,7 @@ func sameType(x, y types.Type) bool { return y != nil && types.Identical(x, y) } -func (x iface) eq(t types.Type, _y interface{}) bool { +func (x iface) eq(t types.Type, _y any) bool { y := _y.(iface) return sameType(x.t, y.t) && (x.t == nil || equals(x.t, x.v, y.v)) } @@ -188,7 +188,7 @@ func (x rtype) hash(_ types.Type) int { return hashType(x.t) } -func (x rtype) eq(_ types.Type, y interface{}) bool { +func (x rtype) eq(_ types.Type, y any) bool { return types.Identical(x.t, y.(rtype).t) } diff --git a/go/ssa/mode.go b/go/ssa/mode.go index 8381639a585..61c91452ce2 100644 --- a/go/ssa/mode.go +++ b/go/ssa/mode.go @@ -108,4 +108,4 @@ func (m *BuilderMode) Set(s string) error { } // Get returns m. -func (m BuilderMode) Get() interface{} { return m } +func (m BuilderMode) Get() any { return m } diff --git a/go/ssa/print.go b/go/ssa/print.go index 432c4b05b6d..8b92d08463a 100644 --- a/go/ssa/print.go +++ b/go/ssa/print.go @@ -387,7 +387,7 @@ func (s *MapUpdate) String() string { func (s *DebugRef) String() string { p := s.Parent().Prog.Fset.Position(s.Pos()) - var descr interface{} + var descr any if s.object != nil { descr = s.object // e.g. "var x int" } else { diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index e35e4d79357..97ef886e3cf 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -48,7 +48,7 @@ func mustSanityCheck(fn *Function, reporter io.Writer) { } } -func (s *sanity) diagnostic(prefix, format string, args ...interface{}) { +func (s *sanity) diagnostic(prefix, format string, args ...any) { fmt.Fprintf(s.reporter, "%s: function %s", prefix, s.fn) if s.block != nil { fmt.Fprintf(s.reporter, ", block %s", s.block) @@ -58,12 +58,12 @@ func (s *sanity) diagnostic(prefix, format string, args ...interface{}) { io.WriteString(s.reporter, "\n") } -func (s *sanity) errorf(format string, args ...interface{}) { +func (s *sanity) errorf(format string, args ...any) { s.insane = true s.diagnostic("Error", format, args...) } -func (s *sanity) warnf(format string, args ...interface{}) { +func (s *sanity) warnf(format string, args ...any) { s.diagnostic("Warning", format, args...) } diff --git a/go/ssa/ssautil/load_test.go b/go/ssa/ssautil/load_test.go index 10375a3227f..cf157fe4401 100644 --- a/go/ssa/ssautil/load_test.go +++ b/go/ssa/ssautil/load_test.go @@ -154,7 +154,7 @@ func TestIssue53604(t *testing.T) { e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{ { Name: "golang.org/fake", - Files: map[string]interface{}{ + Files: map[string]any{ "x/x.go": `package x; import "golang.org/fake/y"; var V = y.F()`, "y/y.go": `package y; import "golang.org/fake/z"; var F = func () *int { return &z.Z } `, "z/z.go": `package z; var Z int`, diff --git a/go/ssa/util.go b/go/ssa/util.go index 2a9c9b9d318..9a73984a6a0 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -166,7 +166,7 @@ func declaredWithin(obj types.Object, fn *types.Func) bool { // returns a closure that prints the corresponding "end" message. // Call using 'defer logStack(...)()' to show builder stack on panic. // Don't forget trailing parens! -func logStack(format string, args ...interface{}) func() { +func logStack(format string, args ...any) func() { msg := fmt.Sprintf(format, args...) io.WriteString(os.Stderr, msg) io.WriteString(os.Stderr, "\n") diff --git a/gopls/internal/analysis/deprecated/deprecated.go b/gopls/internal/analysis/deprecated/deprecated.go index c6df00b4f50..400041ba088 100644 --- a/gopls/internal/analysis/deprecated/deprecated.go +++ b/gopls/internal/analysis/deprecated/deprecated.go @@ -36,7 +36,7 @@ var Analyzer = &analysis.Analyzer{ } // checkDeprecated is a simplified copy of staticcheck.CheckDeprecated. -func checkDeprecated(pass *analysis.Pass) (interface{}, error) { +func checkDeprecated(pass *analysis.Pass) (any, error) { inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) deprs, err := collectDeprecatedNames(pass, inspector) diff --git a/gopls/internal/analysis/embeddirective/embeddirective.go b/gopls/internal/analysis/embeddirective/embeddirective.go index e623587cc68..7590cba9ad8 100644 --- a/gopls/internal/analysis/embeddirective/embeddirective.go +++ b/gopls/internal/analysis/embeddirective/embeddirective.go @@ -28,7 +28,7 @@ var Analyzer = &analysis.Analyzer{ const FixCategory = "addembedimport" // recognized by gopls ApplyFix -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { for _, f := range pass.Files { comments := embedDirectiveComments(f) if len(comments) == 0 { diff --git a/gopls/internal/analysis/fillreturns/fillreturns.go b/gopls/internal/analysis/fillreturns/fillreturns.go index b6bcc1f24dc..184aac5ea1f 100644 --- a/gopls/internal/analysis/fillreturns/fillreturns.go +++ b/gopls/internal/analysis/fillreturns/fillreturns.go @@ -36,7 +36,7 @@ var Analyzer = &analysis.Analyzer{ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillreturns", } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) info := pass.TypesInfo diff --git a/gopls/internal/analysis/nonewvars/nonewvars.go b/gopls/internal/analysis/nonewvars/nonewvars.go index 8a3bf502c51..b7f861ba7f1 100644 --- a/gopls/internal/analysis/nonewvars/nonewvars.go +++ b/gopls/internal/analysis/nonewvars/nonewvars.go @@ -32,7 +32,7 @@ var Analyzer = &analysis.Analyzer{ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/nonewvars", } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) for _, typeErr := range pass.TypeErrors { diff --git a/gopls/internal/analysis/noresultvalues/noresultvalues.go b/gopls/internal/analysis/noresultvalues/noresultvalues.go index fe979f52aac..6b8f9d895e4 100644 --- a/gopls/internal/analysis/noresultvalues/noresultvalues.go +++ b/gopls/internal/analysis/noresultvalues/noresultvalues.go @@ -32,7 +32,7 @@ var Analyzer = &analysis.Analyzer{ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/noresultvalues", } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) for _, typErr := range pass.TypeErrors { diff --git a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go index 15176cef1c8..b38ccf4d5ed 100644 --- a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go +++ b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go @@ -33,7 +33,7 @@ var Analyzer = &analysis.Analyzer{ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/simplifycompositelit", } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { // Gather information whether file is generated or not generated := make(map[*token.File]bool) for _, file := range pass.Files { diff --git a/gopls/internal/analysis/simplifyrange/simplifyrange.go b/gopls/internal/analysis/simplifyrange/simplifyrange.go index fd685ba2c5b..594ebd1f55a 100644 --- a/gopls/internal/analysis/simplifyrange/simplifyrange.go +++ b/gopls/internal/analysis/simplifyrange/simplifyrange.go @@ -26,7 +26,7 @@ var Analyzer = &analysis.Analyzer{ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/simplifyrange", } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { // Gather information whether file is generated or not generated := make(map[*token.File]bool) for _, file := range pass.Files { diff --git a/gopls/internal/analysis/simplifyslice/simplifyslice.go b/gopls/internal/analysis/simplifyslice/simplifyslice.go index 6755187afe5..28cc266d713 100644 --- a/gopls/internal/analysis/simplifyslice/simplifyslice.go +++ b/gopls/internal/analysis/simplifyslice/simplifyslice.go @@ -37,7 +37,7 @@ var Analyzer = &analysis.Analyzer{ // An example where it does not: // x, y := b[:n], b[n:] -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { // Gather information whether file is generated or not generated := make(map[*token.File]bool) for _, file := range pass.Files { diff --git a/gopls/internal/analysis/yield/yield.go b/gopls/internal/analysis/yield/yield.go index ccd30045f97..354cf372186 100644 --- a/gopls/internal/analysis/yield/yield.go +++ b/gopls/internal/analysis/yield/yield.go @@ -44,7 +44,7 @@ var Analyzer = &analysis.Analyzer{ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/yield", } -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) // Find all calls to yield of the right type. diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go index a0dd322a51e..4083f49d2d6 100644 --- a/gopls/internal/cache/analysis.go +++ b/gopls/internal/cache/analysis.go @@ -885,7 +885,7 @@ type action struct { vdeps map[PackageID]*analysisNode // vertical dependencies // results of action.exec(): - result interface{} // result of Run function, of type a.ResultType + result any // result of Run function, of type a.ResultType summary *actionSummary err error } @@ -964,7 +964,7 @@ func (act *action) exec(ctx context.Context) (any, *actionSummary, error) { } // Gather analysis Result values from horizontal dependencies. - inputs := make(map[*analysis.Analyzer]interface{}) + inputs := make(map[*analysis.Analyzer]any) for _, dep := range act.hdeps { inputs[dep.a] = dep.result } @@ -1178,7 +1178,7 @@ func (act *action) exec(ctx context.Context) (any, *actionSummary, error) { // Recover from panics (only) within the analyzer logic. // (Use an anonymous function to limit the recover scope.) - var result interface{} + var result any func() { start := time.Now() defer func() { diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go index 140cbc45490..e15e0cef0b6 100644 --- a/gopls/internal/cache/load.go +++ b/gopls/internal/cache/load.go @@ -365,7 +365,7 @@ func (s *Snapshot) config(ctx context.Context, allowNetwork AllowNetwork) *packa packages.NeedForTest, Fset: nil, // we do our own parsing Overlay: s.buildOverlays(), - Logf: func(format string, args ...interface{}) { + Logf: func(format string, args ...any) { if s.view.folder.Options.VerboseOutput { event.Log(ctx, fmt.Sprintf(format, args...)) } diff --git a/gopls/internal/cache/mod.go b/gopls/internal/cache/mod.go index f16cfbfe1af..f6dd22754cc 100644 --- a/gopls/internal/cache/mod.go +++ b/gopls/internal/cache/mod.go @@ -45,14 +45,14 @@ func (s *Snapshot) ParseMod(ctx context.Context, fh file.Handle) (*ParsedModule, // cache miss? if !hit { - promise, release := s.store.Promise(parseModKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} { + promise, release := s.store.Promise(parseModKey(fh.Identity()), func(ctx context.Context, _ any) any { parsed, err := parseModImpl(ctx, fh) return parseModResult{parsed, err} }) entry = promise s.mu.Lock() - s.parseModHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.parseModHandles.Set(uri, entry, func(_, _ any) { release() }) s.mu.Unlock() } @@ -131,14 +131,14 @@ func (s *Snapshot) ParseWork(ctx context.Context, fh file.Handle) (*ParsedWorkFi // cache miss? if !hit { - handle, release := s.store.Promise(parseWorkKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} { + handle, release := s.store.Promise(parseWorkKey(fh.Identity()), func(ctx context.Context, _ any) any { parsed, err := parseWorkImpl(ctx, fh) return parseWorkResult{parsed, err} }) entry = handle s.mu.Lock() - s.parseWorkHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.parseWorkHandles.Set(uri, entry, func(_, _ any) { release() }) s.mu.Unlock() } @@ -212,7 +212,7 @@ func (s *Snapshot) ModWhy(ctx context.Context, fh file.Handle) (map[string]strin // cache miss? if !hit { - handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg interface{}) interface{} { + handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg any) any { why, err := modWhyImpl(ctx, arg.(*Snapshot), fh) return modWhyResult{why, err} }) diff --git a/gopls/internal/cache/mod_tidy.go b/gopls/internal/cache/mod_tidy.go index 4d473d39b12..6d9a3e56b81 100644 --- a/gopls/internal/cache/mod_tidy.go +++ b/gopls/internal/cache/mod_tidy.go @@ -76,7 +76,7 @@ func (s *Snapshot) ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule return nil, err } - handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg interface{}) interface{} { + handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg any) any { tidied, err := modTidyImpl(ctx, arg.(*Snapshot), pm) return modTidyResult{tidied, err} }) diff --git a/gopls/internal/cache/mod_vuln.go b/gopls/internal/cache/mod_vuln.go index a92f5b5abe1..a48b18e4ba4 100644 --- a/gopls/internal/cache/mod_vuln.go +++ b/gopls/internal/cache/mod_vuln.go @@ -40,7 +40,7 @@ func (s *Snapshot) ModVuln(ctx context.Context, modURI protocol.DocumentURI) (*v // Cache miss? if !hit { - handle := memoize.NewPromise("modVuln", func(ctx context.Context, arg interface{}) interface{} { + handle := memoize.NewPromise("modVuln", func(ctx context.Context, arg any) any { result, err := modVulnImpl(ctx, arg.(*Snapshot)) return modVuln{result, err} }) diff --git a/gopls/internal/cache/parse_cache.go b/gopls/internal/cache/parse_cache.go index 8586f655d28..015510b881d 100644 --- a/gopls/internal/cache/parse_cache.go +++ b/gopls/internal/cache/parse_cache.go @@ -195,7 +195,7 @@ func (c *parseCache) startParse(mode parser.Mode, purgeFuncBodies bool, fhs ...f } uri := fh.URI() - promise := memoize.NewPromise("parseCache.parse", func(ctx context.Context, _ interface{}) interface{} { + promise := memoize.NewPromise("parseCache.parse", func(ctx context.Context, _ any) any { // Allocate 2*len(content)+parsePadding to allow for re-parsing once // inside of parseGoSrc without exceeding the allocated space. base, nextBase := c.allocateSpace(2*len(content) + parsePadding) @@ -404,13 +404,13 @@ func (q queue) Swap(i, j int) { q[j].lruIndex = j } -func (q *queue) Push(x interface{}) { +func (q *queue) Push(x any) { e := x.(*parseCacheEntry) e.lruIndex = len(*q) *q = append(*q, e) } -func (q *queue) Pop() interface{} { +func (q *queue) Pop() any { last := len(*q) - 1 e := (*q)[last] (*q)[last] = nil // aid GC diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go index f7ba04df6a4..2a161ad0fc8 100644 --- a/gopls/internal/cmd/cmd.go +++ b/gopls/internal/cmd/cmd.go @@ -369,7 +369,7 @@ func (c *connection) initialize(ctx context.Context, options func(*settings.Opti } params.Capabilities.Window.WorkDoneProgress = true - params.InitializationOptions = map[string]interface{}{ + params.InitializationOptions = map[string]any{ "symbolMatcher": string(opts.SymbolMatcher), } if c.initializeResult, err = c.Initialize(ctx, params); err != nil { @@ -468,7 +468,7 @@ func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams return nil } -func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil } +func (c *cmdClient) Event(ctx context.Context, t *any) error { return nil } func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error { return nil @@ -482,13 +482,13 @@ func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceF return nil, nil } -func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { - results := make([]interface{}, len(p.Items)) +func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]any, error) { + results := make([]any, len(p.Items)) for i, item := range p.Items { if item.Section != "gopls" { continue } - m := map[string]interface{}{ + m := map[string]any{ "analyses": map[string]any{ "fillreturns": true, "nonewvars": true, @@ -658,7 +658,7 @@ func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishD // TODO(golang/go#60122): replace the gopls.diagnose_files // command with support for textDocument/diagnostic, // so that we don't need to do this de-duplication. - type key [6]interface{} + type key [6]any seen := make(map[key]bool) out := file.diagnostics[:0] for _, d := range file.diagnostics { diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index 42812a870a4..986453253f8 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go @@ -930,7 +930,7 @@ package foo res3 := goplsWithEnv(t, tree, []string{"GOPACKAGESDRIVER=off"}, "stats", "-anon") res3.checkExit(true) - var statsAsMap3 map[string]interface{} + var statsAsMap3 map[string]any if err := json.Unmarshal([]byte(res3.stdout), &statsAsMap3); err != nil { t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) } @@ -1212,7 +1212,7 @@ func (res *result) checkOutput(pattern, name, content string) { } // toJSON decodes res.stdout as JSON into to *ptr and reports its success. -func (res *result) toJSON(ptr interface{}) bool { +func (res *result) toJSON(ptr any) bool { if err := json.Unmarshal([]byte(res.stdout), ptr); err != nil { res.t.Errorf("invalid JSON %v", err) return false diff --git a/gopls/internal/cmd/stats.go b/gopls/internal/cmd/stats.go index cc19a94fb84..1ba43ccee83 100644 --- a/gopls/internal/cmd/stats.go +++ b/gopls/internal/cmd/stats.go @@ -164,7 +164,7 @@ func (s *stats) Run(ctx context.Context, args ...string) error { } // Filter JSON output to fields that are consistent with s.Anon. - okFields := make(map[string]interface{}) + okFields := make(map[string]any) { v := reflect.ValueOf(stats) t := v.Type() diff --git a/gopls/internal/cmd/symbols.go b/gopls/internal/cmd/symbols.go index 663a08f4be1..15c593b0e74 100644 --- a/gopls/internal/cmd/symbols.go +++ b/gopls/internal/cmd/symbols.go @@ -53,7 +53,7 @@ func (r *symbols) Run(ctx context.Context, args ...string) error { return err } for _, s := range symbols { - if m, ok := s.(map[string]interface{}); ok { + if m, ok := s.(map[string]any); ok { s, err = mapToSymbol(m) if err != nil { return err @@ -69,7 +69,7 @@ func (r *symbols) Run(ctx context.Context, args ...string) error { return nil } -func mapToSymbol(m map[string]interface{}) (interface{}, error) { +func mapToSymbol(m map[string]any) (any, error) { b, err := json.Marshal(m) if err != nil { return nil, err diff --git a/gopls/internal/debug/log/log.go b/gopls/internal/debug/log/log.go index d015f9bfdd3..9e7efa7bf17 100644 --- a/gopls/internal/debug/log/log.go +++ b/gopls/internal/debug/log/log.go @@ -33,7 +33,7 @@ func (l Level) Log(ctx context.Context, msg string) { } // Logf formats and exports a log event labeled with level l. -func (l Level) Logf(ctx context.Context, format string, args ...interface{}) { +func (l Level) Logf(ctx context.Context, format string, args ...any) { l.Log(ctx, fmt.Sprintf(format, args...)) } diff --git a/gopls/internal/debug/rpc.go b/gopls/internal/debug/rpc.go index 8a696f848d0..5b8e1dbbbd0 100644 --- a/gopls/internal/debug/rpc.go +++ b/gopls/internal/debug/rpc.go @@ -209,7 +209,7 @@ func getStatusCode(span *export.Span) string { return "" } -func (r *Rpcs) getData(req *http.Request) interface{} { +func (r *Rpcs) getData(req *http.Request) any { return r } diff --git a/gopls/internal/debug/serve.go b/gopls/internal/debug/serve.go index 058254b755b..c471f488cd1 100644 --- a/gopls/internal/debug/serve.go +++ b/gopls/internal/debug/serve.go @@ -280,23 +280,23 @@ func cmdline(w http.ResponseWriter, r *http.Request) { pprof.Cmdline(fake, r) } -func (i *Instance) getCache(r *http.Request) interface{} { +func (i *Instance) getCache(r *http.Request) any { return i.State.Cache(path.Base(r.URL.Path)) } -func (i *Instance) getAnalysis(r *http.Request) interface{} { +func (i *Instance) getAnalysis(r *http.Request) any { return i.State.Analysis() } -func (i *Instance) getSession(r *http.Request) interface{} { +func (i *Instance) getSession(r *http.Request) any { return i.State.Session(path.Base(r.URL.Path)) } -func (i *Instance) getClient(r *http.Request) interface{} { +func (i *Instance) getClient(r *http.Request) any { return i.State.Client(path.Base(r.URL.Path)) } -func (i *Instance) getServer(r *http.Request) interface{} { +func (i *Instance) getServer(r *http.Request) any { i.State.mu.Lock() defer i.State.mu.Unlock() id := path.Base(r.URL.Path) @@ -308,7 +308,7 @@ func (i *Instance) getServer(r *http.Request) interface{} { return nil } -func (i *Instance) getFile(r *http.Request) interface{} { +func (i *Instance) getFile(r *http.Request) any { identifier := path.Base(r.URL.Path) sid := path.Base(path.Dir(r.URL.Path)) s := i.State.Session(sid) @@ -324,7 +324,7 @@ func (i *Instance) getFile(r *http.Request) interface{} { return nil } -func (i *Instance) getInfo(r *http.Request) interface{} { +func (i *Instance) getInfo(r *http.Request) any { buf := &bytes.Buffer{} i.PrintServerInfo(r.Context(), buf) return template.HTML(buf.String()) @@ -340,7 +340,7 @@ func (i *Instance) AddService(s protocol.Server, session *cache.Session) { stdlog.Printf("unable to find a Client to add the protocol.Server to") } -func getMemory(_ *http.Request) interface{} { +func getMemory(_ *http.Request) any { var m runtime.MemStats runtime.ReadMemStats(&m) return m @@ -439,7 +439,7 @@ func (i *Instance) Serve(ctx context.Context, addr string) (string, error) { event.Log(ctx, "Debug serving", label1.Port.Of(port)) go func() { mux := http.NewServeMux() - mux.HandleFunc("/", render(MainTmpl, func(*http.Request) interface{} { return i })) + mux.HandleFunc("/", render(MainTmpl, func(*http.Request) any { return i })) mux.HandleFunc("/debug/", render(DebugTmpl, nil)) mux.HandleFunc("/debug/pprof/", pprof.Index) mux.HandleFunc("/debug/pprof/cmdline", cmdline) @@ -594,11 +594,11 @@ func makeInstanceExporter(i *Instance) event.Exporter { return exporter } -type dataFunc func(*http.Request) interface{} +type dataFunc func(*http.Request) any func render(tmpl *template.Template, fun dataFunc) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - var data interface{} + var data any if fun != nil { data = fun(r) } diff --git a/gopls/internal/debug/template_test.go b/gopls/internal/debug/template_test.go index d4d9071c140..52c60244776 100644 --- a/gopls/internal/debug/template_test.go +++ b/gopls/internal/debug/template_test.go @@ -29,7 +29,7 @@ import ( var templates = map[string]struct { tmpl *template.Template - data interface{} // a value of the needed type + data any // a value of the needed type }{ "MainTmpl": {debug.MainTmpl, &debug.Instance{}}, "DebugTmpl": {debug.DebugTmpl, nil}, diff --git a/gopls/internal/debug/trace.go b/gopls/internal/debug/trace.go index 9314a04d241..e6ff9697b67 100644 --- a/gopls/internal/debug/trace.go +++ b/gopls/internal/debug/trace.go @@ -277,7 +277,7 @@ func (t *traces) addRecentLocked(span *traceSpan, start bool) { } // getData returns the TraceResults rendered by TraceTmpl for the /trace[/name] endpoint. -func (t *traces) getData(req *http.Request) interface{} { +func (t *traces) getData(req *http.Request) any { // TODO(adonovan): the HTTP request doesn't acquire the mutex // for t or for each span! Audit and fix. diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go index 280795abe5e..97423fe87a7 100644 --- a/gopls/internal/golang/rename_check.go +++ b/gopls/internal/golang/rename_check.go @@ -51,7 +51,7 @@ import ( ) // errorf reports an error (e.g. conflict) and prevents file modification. -func (r *renamer) errorf(pos token.Pos, format string, args ...interface{}) { +func (r *renamer) errorf(pos token.Pos, format string, args ...any) { // Conflict error messages in the old gorename tool (whence this // logic originated) contain rich information associated with // multiple source lines, such as: diff --git a/gopls/internal/lsprpc/binder_test.go b/gopls/internal/lsprpc/binder_test.go index 042056e7777..07a8b2cdf99 100644 --- a/gopls/internal/lsprpc/binder_test.go +++ b/gopls/internal/lsprpc/binder_test.go @@ -56,7 +56,7 @@ func (b *ServerBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) j serverHandler := protocol.ServerHandlerV2(server) // Wrap the server handler to inject the client into each request context, so // that log events are reflected back to the client. - wrapped := jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { + wrapped := jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (any, error) { ctx = protocol.WithClient(ctx, client) return serverHandler.Handle(ctx, req) }) diff --git a/gopls/internal/lsprpc/commandinterceptor_test.go b/gopls/internal/lsprpc/commandinterceptor_test.go index 7c83ef993f0..3cfa2e35a7f 100644 --- a/gopls/internal/lsprpc/commandinterceptor_test.go +++ b/gopls/internal/lsprpc/commandinterceptor_test.go @@ -15,9 +15,9 @@ import ( . "golang.org/x/tools/gopls/internal/lsprpc" ) -func CommandInterceptor(command string, run func(*protocol.ExecuteCommandParams) (interface{}, error)) Middleware { +func CommandInterceptor(command string, run func(*protocol.ExecuteCommandParams) (any, error)) Middleware { return BindHandler(func(delegate jsonrpc2_v2.Handler) jsonrpc2_v2.Handler { - return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { + return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (any, error) { if req.Method == "workspace/executeCommand" { var params protocol.ExecuteCommandParams if err := json.Unmarshal(req.Params, ¶ms); err == nil { @@ -35,9 +35,9 @@ func CommandInterceptor(command string, run func(*protocol.ExecuteCommandParams) func TestCommandInterceptor(t *testing.T) { const command = "foo" caught := false - intercept := func(_ *protocol.ExecuteCommandParams) (interface{}, error) { + intercept := func(_ *protocol.ExecuteCommandParams) (any, error) { caught = true - return map[string]interface{}{}, nil + return map[string]any{}, nil } ctx := context.Background() @@ -50,7 +50,7 @@ func TestCommandInterceptor(t *testing.T) { params := &protocol.ExecuteCommandParams{ Command: command, } - var res interface{} + var res any err := conn.Call(ctx, "workspace/executeCommand", params).Await(ctx, &res) if err != nil { t.Fatal(err) diff --git a/gopls/internal/lsprpc/export_test.go b/gopls/internal/lsprpc/export_test.go index 509129870dc..8cbdecc98a2 100644 --- a/gopls/internal/lsprpc/export_test.go +++ b/gopls/internal/lsprpc/export_test.go @@ -26,7 +26,7 @@ type Canceler struct { Conn *jsonrpc2_v2.Connection } -func (c *Canceler) Preempt(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { +func (c *Canceler) Preempt(ctx context.Context, req *jsonrpc2_v2.Request) (any, error) { if req.Method != "$/cancelRequest" { return nil, jsonrpc2_v2.ErrNotHandled } @@ -65,7 +65,7 @@ func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) serverConn, err := jsonrpc2_v2.Dial(context.Background(), b.dialer, clientBinder) if err != nil { return jsonrpc2_v2.ConnectionOptions{ - Handler: jsonrpc2_v2.HandlerFunc(func(context.Context, *jsonrpc2_v2.Request) (interface{}, error) { + Handler: jsonrpc2_v2.HandlerFunc(func(context.Context, *jsonrpc2_v2.Request) (any, error) { return nil, fmt.Errorf("%w: %v", jsonrpc2_v2.ErrInternal, err) }), } diff --git a/gopls/internal/lsprpc/goenv.go b/gopls/internal/lsprpc/goenv.go index 52ec08ff7eb..2b8b94345ca 100644 --- a/gopls/internal/lsprpc/goenv.go +++ b/gopls/internal/lsprpc/goenv.go @@ -12,7 +12,7 @@ import ( "golang.org/x/tools/internal/gocommand" ) -func getGoEnv(ctx context.Context, env map[string]interface{}) (map[string]string, error) { +func getGoEnv(ctx context.Context, env map[string]any) (map[string]string, error) { var runEnv []string for k, v := range env { runEnv = append(runEnv, fmt.Sprintf("%s=%s", k, v)) diff --git a/gopls/internal/lsprpc/goenv_test.go b/gopls/internal/lsprpc/goenv_test.go index 6c41540fafb..bc39228c614 100644 --- a/gopls/internal/lsprpc/goenv_test.go +++ b/gopls/internal/lsprpc/goenv_test.go @@ -21,7 +21,7 @@ import ( func GoEnvMiddleware() (Middleware, error) { return BindHandler(func(delegate jsonrpc2_v2.Handler) jsonrpc2_v2.Handler { - return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { + return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (any, error) { if req.Method == "initialize" { if err := addGoEnvToInitializeRequestV2(ctx, req); err != nil { event.Error(ctx, "adding go env to initialize", err) @@ -39,20 +39,20 @@ func addGoEnvToInitializeRequestV2(ctx context.Context, req *jsonrpc2_v2.Request if err := json.Unmarshal(req.Params, ¶ms); err != nil { return err } - var opts map[string]interface{} + var opts map[string]any switch v := params.InitializationOptions.(type) { case nil: - opts = make(map[string]interface{}) - case map[string]interface{}: + opts = make(map[string]any) + case map[string]any: opts = v default: return fmt.Errorf("unexpected type for InitializationOptions: %T", v) } envOpt, ok := opts["env"] if !ok { - envOpt = make(map[string]interface{}) + envOpt = make(map[string]any) } - env, ok := envOpt.(map[string]interface{}) + env, ok := envOpt.(map[string]any) if !ok { return fmt.Errorf("env option is %T, expected a map", envOpt) } @@ -108,8 +108,8 @@ func TestGoEnvMiddleware(t *testing.T) { conn := env.dial(ctx, t, l.Dialer(), noopBinder, true) dispatch := protocol.ServerDispatcherV2(conn) initParams := &protocol.ParamInitialize{} - initParams.InitializationOptions = map[string]interface{}{ - "env": map[string]interface{}{ + initParams.InitializationOptions = map[string]any{ + "env": map[string]any{ "GONOPROXY": "example.com", }, } @@ -120,7 +120,7 @@ func TestGoEnvMiddleware(t *testing.T) { if server.params == nil { t.Fatalf("initialize params are unset") } - envOpts := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{}) + envOpts := server.params.InitializationOptions.(map[string]any)["env"].(map[string]any) // Check for an arbitrary Go variable. It should be set. if _, ok := envOpts["GOPRIVATE"]; !ok { diff --git a/gopls/internal/lsprpc/lsprpc.go b/gopls/internal/lsprpc/lsprpc.go index b77557c9a4b..9255f9176bc 100644 --- a/gopls/internal/lsprpc/lsprpc.go +++ b/gopls/internal/lsprpc/lsprpc.go @@ -323,20 +323,20 @@ func addGoEnvToInitializeRequest(ctx context.Context, r jsonrpc2.Request) (jsonr if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return nil, err } - var opts map[string]interface{} + var opts map[string]any switch v := params.InitializationOptions.(type) { case nil: - opts = make(map[string]interface{}) - case map[string]interface{}: + opts = make(map[string]any) + case map[string]any: opts = v default: return nil, fmt.Errorf("unexpected type for InitializationOptions: %T", v) } envOpt, ok := opts["env"] if !ok { - envOpt = make(map[string]interface{}) + envOpt = make(map[string]any) } - env, ok := envOpt.(map[string]interface{}) + env, ok := envOpt.(map[string]any) if !ok { return nil, fmt.Errorf(`env option is %T, expected a map`, envOpt) } @@ -368,7 +368,7 @@ func (f *forwarder) replyWithDebugAddress(outerCtx context.Context, r jsonrpc2.R event.Log(outerCtx, "no debug instance to start") return r } - return func(ctx context.Context, result interface{}, outerErr error) error { + return func(ctx context.Context, result any, outerErr error) error { if outerErr != nil { return r(ctx, result, outerErr) } diff --git a/gopls/internal/lsprpc/lsprpc_test.go b/gopls/internal/lsprpc/lsprpc_test.go index c4ccab71a3e..1a259bbd646 100644 --- a/gopls/internal/lsprpc/lsprpc_test.go +++ b/gopls/internal/lsprpc/lsprpc_test.go @@ -302,8 +302,8 @@ func TestEnvForwarding(t *testing.T) { conn.Go(ctx, jsonrpc2.MethodNotFound) dispatch := protocol.ServerDispatcher(conn) initParams := &protocol.ParamInitialize{} - initParams.InitializationOptions = map[string]interface{}{ - "env": map[string]interface{}{ + initParams.InitializationOptions = map[string]any{ + "env": map[string]any{ "GONOPROXY": "example.com", }, } @@ -314,7 +314,7 @@ func TestEnvForwarding(t *testing.T) { if server.params == nil { t.Fatalf("initialize params are unset") } - env := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{}) + env := server.params.InitializationOptions.(map[string]any)["env"].(map[string]any) // Check for an arbitrary Go variable. It should be set. if _, ok := env["GOPRIVATE"]; !ok { diff --git a/gopls/internal/lsprpc/middleware_test.go b/gopls/internal/lsprpc/middleware_test.go index 526c7343b78..afa6ae78d2f 100644 --- a/gopls/internal/lsprpc/middleware_test.go +++ b/gopls/internal/lsprpc/middleware_test.go @@ -154,7 +154,7 @@ func (h *Handshaker) Middleware(inner jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { // Wrap the delegated handler to accept the handshake. delegate := opts.Handler - opts.Handler = jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { + opts.Handler = jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (any, error) { if req.Method == HandshakeMethod { var peerInfo PeerInfo if err := json.Unmarshal(req.Params, &peerInfo); err != nil { diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 007b8d5218f..0142de532c3 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -46,7 +46,7 @@ import ( "golang.org/x/tools/internal/xcontext" ) -func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { +func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (any, error) { ctx, done := event.Start(ctx, "lsp.Server.executeCommand") defer done() diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index de6b764c79f..b7b69931103 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go @@ -104,7 +104,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ } s.pendingFolders = append(s.pendingFolders, folders...) - var codeActionProvider interface{} = true + var codeActionProvider any = true if ca := params.Capabilities.TextDocument.CodeAction; len(ca.CodeActionLiteralSupport.CodeActionKind.ValueSet) > 0 { // If the client has specified CodeActionLiteralSupport, // send the code actions we support. @@ -126,7 +126,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ } } - var renameOpts interface{} = true + var renameOpts any = true if r := params.Capabilities.TextDocument.Rename; r != nil && r.PrepareSupport { renameOpts = protocol.RenameOptions{ PrepareProvider: r.PrepareSupport, diff --git a/gopls/internal/server/unimplemented.go b/gopls/internal/server/unimplemented.go index 470a7cbb0ee..7375dc4bb1b 100644 --- a/gopls/internal/server/unimplemented.go +++ b/gopls/internal/server/unimplemented.go @@ -114,7 +114,7 @@ func (s *server) ResolveWorkspaceSymbol(context.Context, *protocol.WorkspaceSymb return nil, notImplemented("ResolveWorkspaceSymbol") } -func (s *server) SemanticTokensFullDelta(context.Context, *protocol.SemanticTokensDeltaParams) (interface{}, error) { +func (s *server) SemanticTokensFullDelta(context.Context, *protocol.SemanticTokensDeltaParams) (any, error) { return nil, notImplemented("SemanticTokensFullDelta") } diff --git a/gopls/internal/template/parse.go b/gopls/internal/template/parse.go index 448a5ab51e8..f1b26bbb14f 100644 --- a/gopls/internal/template/parse.go +++ b/gopls/internal/template/parse.go @@ -114,7 +114,7 @@ func parseBuffer(buf []byte) *Parsed { matches := parseErrR.FindStringSubmatch(err.Error()) if len(matches) == 2 { // suppress the error by giving it a function with the right name - funcs[matches[1]] = func() interface{} { return nil } + funcs[matches[1]] = func() any { return nil } t, err = template.New("").Funcs(funcs).Parse(string(ans.buf)) continue } diff --git a/gopls/internal/test/integration/bench/completion_test.go b/gopls/internal/test/integration/bench/completion_test.go index bbbba0e3fd1..d84512d1f8f 100644 --- a/gopls/internal/test/integration/bench/completion_test.go +++ b/gopls/internal/test/integration/bench/completion_test.go @@ -282,7 +282,7 @@ func runCompletion(b *testing.B, test completionTest, followingEdit, completeUni env := repo.newEnv(b, fake.EditorConfig{ Env: envvars, - Settings: map[string]interface{}{ + Settings: map[string]any{ "completeUnimported": completeUnimported, "completionBudget": budget, }, diff --git a/gopls/internal/test/integration/bench/didchange_test.go b/gopls/internal/test/integration/bench/didchange_test.go index 57ed01bbcd6..b1613bb1b03 100644 --- a/gopls/internal/test/integration/bench/didchange_test.go +++ b/gopls/internal/test/integration/bench/didchange_test.go @@ -118,7 +118,7 @@ func runChangeDiagnosticsBenchmark(b *testing.B, test changeTest, save bool, ope Env: map[string]string{ "GOPATH": sharedEnv.Sandbox.GOPATH(), }, - Settings: map[string]interface{}{ + Settings: map[string]any{ "diagnosticsDelay": "0s", }, } diff --git a/gopls/internal/test/integration/env.go b/gopls/internal/test/integration/env.go index 64344d0d146..c8a1b5043aa 100644 --- a/gopls/internal/test/integration/env.go +++ b/gopls/internal/test/integration/env.go @@ -282,7 +282,7 @@ func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) erro if !ok { panic(fmt.Sprintf("got progress report for unknown report %v: %v", m.Token, m)) } - v := m.Value.(map[string]interface{}) + v := m.Value.(map[string]any) switch kind := v["kind"]; kind { case "begin": work.title = v["title"].(string) diff --git a/gopls/internal/test/integration/env_test.go b/gopls/internal/test/integration/env_test.go index 32203f7cb83..1fa68676b5c 100644 --- a/gopls/internal/test/integration/env_test.go +++ b/gopls/internal/test/integration/env_test.go @@ -33,7 +33,7 @@ func TestProgressUpdating(t *testing.T) { } updates := []struct { token string - value interface{} + value any }{ {"foo", protocol.WorkDoneProgressBegin{Kind: "begin", Title: "foo work"}}, {"bar", protocol.WorkDoneProgressBegin{Kind: "begin", Title: "bar work"}}, diff --git a/gopls/internal/test/integration/expectation.go b/gopls/internal/test/integration/expectation.go index ad41423d098..fdfca90796e 100644 --- a/gopls/internal/test/integration/expectation.go +++ b/gopls/internal/test/integration/expectation.go @@ -677,7 +677,7 @@ func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict { rec := regexp.MustCompile(re) return func(s State) Verdict { r := s.registeredCapabilities["workspace/didChangeWatchedFiles"] - watchers := jsonProperty(r.RegisterOptions, "watchers").([]interface{}) + watchers := jsonProperty(r.RegisterOptions, "watchers").([]any) for _, watcher := range watchers { pattern := jsonProperty(watcher, "globPattern").(string) if rec.MatchString(pattern) { @@ -699,11 +699,11 @@ func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict { // } // // Then jsonProperty(obj, "foo", "bar") will be 3. -func jsonProperty(obj interface{}, path ...string) interface{} { +func jsonProperty(obj any, path ...string) any { if len(path) == 0 || obj == nil { return obj } - m := obj.(map[string]interface{}) + m := obj.(map[string]any) return jsonProperty(m[path[0]], path[1:]...) } diff --git a/gopls/internal/test/integration/fake/client.go b/gopls/internal/test/integration/fake/client.go index 93eeab4a8af..aee6c1cfc3e 100644 --- a/gopls/internal/test/integration/fake/client.go +++ b/gopls/internal/test/integration/fake/client.go @@ -103,7 +103,7 @@ func (c *Client) LogMessage(ctx context.Context, params *protocol.LogMessagePara return nil } -func (c *Client) Event(ctx context.Context, event *interface{}) error { +func (c *Client) Event(ctx context.Context, event *any) error { return nil } @@ -118,8 +118,8 @@ func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, return []protocol.WorkspaceFolder{}, nil } -func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { - results := make([]interface{}, len(p.Items)) +func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]any, error) { + results := make([]any, len(p.Items)) for i, item := range p.Items { if item.ScopeURI != nil && *item.ScopeURI == "" { return nil, fmt.Errorf(`malformed ScopeURI ""`) diff --git a/gopls/internal/test/integration/fake/glob/glob.go b/gopls/internal/test/integration/fake/glob/glob.go index a540ebefac5..3bda93bee6d 100644 --- a/gopls/internal/test/integration/fake/glob/glob.go +++ b/gopls/internal/test/integration/fake/glob/glob.go @@ -217,7 +217,7 @@ func (g *Glob) Match(input string) bool { } func match(elems []element, input string) (ok bool) { - var elem interface{} + var elem any for len(elems) > 0 { elem, elems = elems[0], elems[1:] switch elem := elem.(type) { diff --git a/gopls/internal/test/integration/options.go b/gopls/internal/test/integration/options.go index 8090388e17d..11824aa7c16 100644 --- a/gopls/internal/test/integration/options.go +++ b/gopls/internal/test/integration/options.go @@ -25,7 +25,7 @@ type runConfig struct { func defaultConfig() runConfig { return runConfig{ editor: fake.EditorConfig{ - Settings: map[string]interface{}{ + Settings: map[string]any{ // Shorten the diagnostic delay to speed up test execution (else we'd add // the default delay to each assertion about diagnostics) "diagnosticsDelay": "10ms", @@ -109,11 +109,11 @@ func CapabilitiesJSON(capabilities []byte) RunOption { // // As a special case, the env setting must not be provided via Settings: use // EnvVars instead. -type Settings map[string]interface{} +type Settings map[string]any func (s Settings) set(opts *runConfig) { if opts.editor.Settings == nil { - opts.editor.Settings = make(map[string]interface{}) + opts.editor.Settings = make(map[string]any) } for k, v := range s { opts.editor.Settings[k] = v diff --git a/gopls/internal/util/bug/bug.go b/gopls/internal/util/bug/bug.go index dcd242d4856..265ec9dac10 100644 --- a/gopls/internal/util/bug/bug.go +++ b/gopls/internal/util/bug/bug.go @@ -50,13 +50,13 @@ type Bug struct { } // Reportf reports a formatted bug message. -func Reportf(format string, args ...interface{}) { +func Reportf(format string, args ...any) { report(fmt.Sprintf(format, args...)) } // Errorf calls fmt.Errorf for the given arguments, and reports the resulting // error message as a bug. -func Errorf(format string, args ...interface{}) error { +func Errorf(format string, args ...any) error { err := fmt.Errorf(format, args...) report(err.Error()) return err diff --git a/gopls/internal/vulncheck/vulntest/report.go b/gopls/internal/vulncheck/vulntest/report.go index 6aa87221866..3b1bfcc5c96 100644 --- a/gopls/internal/vulncheck/vulntest/report.go +++ b/gopls/internal/vulncheck/vulntest/report.go @@ -134,7 +134,7 @@ func (v Version) Canonical() string { // single-element mapping of type to URL. type Reference osv.Reference -func (r *Reference) MarshalYAML() (interface{}, error) { +func (r *Reference) MarshalYAML() (any, error) { return map[string]string{ strings.ToLower(string(r.Type)): r.URL, }, nil diff --git a/internal/event/export/id.go b/internal/event/export/id.go index bf9938b38c1..fb6026462c1 100644 --- a/internal/event/export/id.go +++ b/internal/event/export/id.go @@ -39,7 +39,7 @@ var ( func initGenerator() { var rngSeed int64 - for _, p := range []interface{}{ + for _, p := range []any{ &rngSeed, &traceIDAdd, &nextSpanID, &spanIDInc, } { binary.Read(crand.Reader, binary.LittleEndian, p) diff --git a/internal/event/export/metric/exporter.go b/internal/event/export/metric/exporter.go index 4cafaa52928..588b8a108c7 100644 --- a/internal/event/export/metric/exporter.go +++ b/internal/event/export/metric/exporter.go @@ -19,14 +19,14 @@ import ( var Entries = keys.New("metric_entries", "The set of metrics calculated for an event") type Config struct { - subscribers map[interface{}][]subscriber + subscribers map[any][]subscriber } type subscriber func(time.Time, label.Map, label.Label) Data func (e *Config) subscribe(key label.Key, s subscriber) { if e.subscribers == nil { - e.subscribers = make(map[interface{}][]subscriber) + e.subscribers = make(map[any][]subscriber) } e.subscribers[key] = append(e.subscribers[key], s) } diff --git a/internal/event/export/ocagent/ocagent.go b/internal/event/export/ocagent/ocagent.go index 722a7446939..d86c4aed0cf 100644 --- a/internal/event/export/ocagent/ocagent.go +++ b/internal/event/export/ocagent/ocagent.go @@ -167,7 +167,7 @@ func (cfg *Config) buildNode() *wire.Node { } } -func (e *Exporter) send(endpoint string, message interface{}) { +func (e *Exporter) send(endpoint string, message any) { blob, err := json.Marshal(message) if err != nil { errorInExport("ocagent failed to marshal message for %v: %v", endpoint, err) @@ -190,7 +190,7 @@ func (e *Exporter) send(endpoint string, message interface{}) { } } -func errorInExport(message string, args ...interface{}) { +func errorInExport(message string, args ...any) { // This function is useful when debugging the exporter, but in general we // want to just drop any export } diff --git a/internal/event/export/prometheus/prometheus.go b/internal/event/export/prometheus/prometheus.go index 0281f60a35f..82bb6c15dfc 100644 --- a/internal/event/export/prometheus/prometheus.go +++ b/internal/event/export/prometheus/prometheus.go @@ -66,7 +66,7 @@ func (e *Exporter) header(w http.ResponseWriter, name, description string, isGau fmt.Fprintf(w, "# TYPE %s %s\n", name, kind) } -func (e *Exporter) row(w http.ResponseWriter, name string, group []label.Label, extra string, value interface{}) { +func (e *Exporter) row(w http.ResponseWriter, name string, group []label.Label, extra string, value any) { fmt.Fprint(w, name) buf := &bytes.Buffer{} fmt.Fprint(buf, group) diff --git a/internal/event/keys/keys.go b/internal/event/keys/keys.go index a02206e3015..4cfa51b6123 100644 --- a/internal/event/keys/keys.go +++ b/internal/event/keys/keys.go @@ -32,7 +32,7 @@ func (k *Value) Format(w io.Writer, buf []byte, l label.Label) { } // Get can be used to get a label for the key from a label.Map. -func (k *Value) Get(lm label.Map) interface{} { +func (k *Value) Get(lm label.Map) any { if t := lm.Find(k); t.Valid() { return k.From(t) } @@ -40,10 +40,10 @@ func (k *Value) Get(lm label.Map) interface{} { } // From can be used to get a value from a Label. -func (k *Value) From(t label.Label) interface{} { return t.UnpackValue() } +func (k *Value) From(t label.Label) any { return t.UnpackValue() } // Of creates a new Label with this key and the supplied value. -func (k *Value) Of(value interface{}) label.Label { return label.OfValue(k, value) } +func (k *Value) Of(value any) label.Label { return label.OfValue(k, value) } // Tag represents a key for tagging labels that have no value. // These are used when the existence of the label is the entire information it diff --git a/internal/event/label/label.go b/internal/event/label/label.go index 0f526e1f9ab..7c00ca2a6da 100644 --- a/internal/event/label/label.go +++ b/internal/event/label/label.go @@ -32,7 +32,7 @@ type Key interface { type Label struct { key Key packed uint64 - untyped interface{} + untyped any } // Map is the interface to a collection of Labels indexed by key. @@ -76,13 +76,13 @@ type mapChain struct { // OfValue creates a new label from the key and value. // This method is for implementing new key types, label creation should // normally be done with the Of method of the key. -func OfValue(k Key, value interface{}) Label { return Label{key: k, untyped: value} } +func OfValue(k Key, value any) Label { return Label{key: k, untyped: value} } // UnpackValue assumes the label was built using LabelOfValue and returns the value // that was passed to that constructor. // 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) UnpackValue() interface{} { return t.untyped } +func (t Label) UnpackValue() any { return t.untyped } // Of64 creates a new label from a key and a uint64. This is often // used for non uint64 values that can be packed into a uint64. diff --git a/internal/expect/expect.go b/internal/expect/expect.go index d977ea4e262..69875cd6585 100644 --- a/internal/expect/expect.go +++ b/internal/expect/expect.go @@ -86,7 +86,7 @@ type ReadFile func(filename string) ([]byte, error) // MatchBefore returns the range of the line that matched the pattern, and // invalid positions if there was no match, or an error if the line could not be // found. -func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern interface{}) (token.Pos, token.Pos, error) { +func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern any) (token.Pos, token.Pos, error) { f := fset.File(end) content, err := readFile(f.Name()) if err != nil { diff --git a/internal/expect/expect_test.go b/internal/expect/expect_test.go index 3ad8d1a74fa..e8f8b6a7a07 100644 --- a/internal/expect/expect_test.go +++ b/internal/expect/expect_test.go @@ -155,7 +155,7 @@ func TestMarker(t *testing.T) { } } -func checkMarker(t *testing.T, fset *token.FileSet, readFile expect.ReadFile, markers map[string]token.Pos, pos token.Pos, name string, pattern interface{}) { +func checkMarker(t *testing.T, fset *token.FileSet, readFile expect.ReadFile, markers map[string]token.Pos, pos token.Pos, name string, pattern any) { start, end, err := expect.MatchBefore(fset, readFile, pos, pattern) if err != nil { t.Errorf("%v: MatchBefore failed: %v", fset.Position(pos), err) diff --git a/internal/expect/extract.go b/internal/expect/extract.go index 1fb4349c48e..150a2afbbf6 100644 --- a/internal/expect/extract.go +++ b/internal/expect/extract.go @@ -32,7 +32,7 @@ type Identifier string // See the package documentation for details about the syntax of those // notes. func Parse(fset *token.FileSet, filename string, content []byte) ([]*Note, error) { - var src interface{} + var src any if content != nil { src = content } @@ -220,7 +220,7 @@ func (t *tokens) Pos() token.Pos { return t.base + token.Pos(t.scanner.Position.Offset) } -func (t *tokens) Errorf(msg string, args ...interface{}) { +func (t *tokens) Errorf(msg string, args ...any) { if t.err != nil { return } diff --git a/internal/facts/facts.go b/internal/facts/facts.go index e1c18d373c3..8e2997e6def 100644 --- a/internal/facts/facts.go +++ b/internal/facts/facts.go @@ -209,7 +209,7 @@ func (d *Decoder) Decode(read func(pkgPath string) ([]byte, error)) (*Set, error // Facts may describe indirectly imported packages, or their objects. m := make(map[key]analysis.Fact) // one big bucket for _, imp := range d.pkg.Imports() { - logf := func(format string, args ...interface{}) { + logf := func(format string, args ...any) { if debug { prefix := fmt.Sprintf("in %s, importing %s: ", d.pkg.Path(), imp.Path()) diff --git a/internal/gcimporter/bimport.go b/internal/gcimporter/bimport.go index d79a605ed13..734c46198df 100644 --- a/internal/gcimporter/bimport.go +++ b/internal/gcimporter/bimport.go @@ -14,7 +14,7 @@ import ( "sync" ) -func errorf(format string, args ...interface{}) { +func errorf(format string, args ...any) { panic(fmt.Sprintf(format, args...)) } diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index 7dfc31a37d7..253d6493c21 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -310,7 +310,7 @@ func IImportShallow(fset *token.FileSet, getPackages GetPackagesFunc, data []byt } // ReportFunc is the type of a function used to report formatted bugs. -type ReportFunc = func(string, ...interface{}) +type ReportFunc = func(string, ...any) // Current bundled export format version. Increase with each format change. // 0: initial implementation @@ -597,7 +597,7 @@ type filePositions struct { needed []uint64 // unordered list of needed file offsets } -func (p *iexporter) trace(format string, args ...interface{}) { +func (p *iexporter) trace(format string, args ...any) { if !trace { // Call sites should also be guarded, but having this check here allows // easily enabling/disabling debug trace statements. @@ -1583,6 +1583,6 @@ func (e internalError) Error() string { return "gcimporter: " + string(e) } // "internalErrorf" as the former is used for bugs, whose cause is // internal inconsistency, whereas the latter is used for ordinary // situations like bad input, whose cause is external. -func internalErrorf(format string, args ...interface{}) error { +func internalErrorf(format string, args ...any) error { return internalError(fmt.Sprintf(format, args...)) } diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index 12943927159..bc6c9741e7d 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -400,7 +400,7 @@ type iimporter struct { indent int // for tracing support } -func (p *iimporter) trace(format string, args ...interface{}) { +func (p *iimporter) trace(format string, args ...any) { if !trace { // Call sites should also be guarded, but having this check here allows // easily enabling/disabling debug trace statements. diff --git a/internal/gopathwalk/walk.go b/internal/gopathwalk/walk.go index 8361515519f..984b79c2a07 100644 --- a/internal/gopathwalk/walk.go +++ b/internal/gopathwalk/walk.go @@ -22,7 +22,7 @@ import ( // Options controls the behavior of a Walk call. type Options struct { // If Logf is non-nil, debug logging is enabled through this function. - Logf func(format string, args ...interface{}) + Logf func(format string, args ...any) // Search module caches. Also disables legacy goimports ignore rules. ModulesEnabled bool @@ -81,7 +81,7 @@ func WalkSkip(roots []Root, add func(root Root, dir string), skip func(root Root // walkDir creates a walker and starts fastwalk with this walker. func walkDir(root Root, add func(Root, string), skip func(root Root, dir string) bool, opts Options) { if opts.Logf == nil { - opts.Logf = func(format string, args ...interface{}) {} + opts.Logf = func(format string, args ...any) {} } if _, err := os.Stat(root.Path); os.IsNotExist(err) { opts.Logf("skipping nonexistent directory: %v", root.Path) diff --git a/internal/imports/fix_test.go b/internal/imports/fix_test.go index 02ddd480dfd..478313aec7f 100644 --- a/internal/imports/fix_test.go +++ b/internal/imports/fix_test.go @@ -1680,7 +1680,7 @@ type testConfig struct { } // fm is the type for a packagestest.Module's Files, abbreviated for shorter lines. -type fm map[string]interface{} +type fm map[string]any func (c testConfig) test(t *testing.T, fn func(*goimportTest)) { t.Helper() diff --git a/internal/jsonrpc2/conn.go b/internal/jsonrpc2/conn.go index 1d76ef9726b..6e8625208d9 100644 --- a/internal/jsonrpc2/conn.go +++ b/internal/jsonrpc2/conn.go @@ -25,12 +25,12 @@ type Conn interface { // The response will be unmarshaled from JSON into the result. // The id returned will be unique from this connection, and can be used for // logging or tracking. - Call(ctx context.Context, method string, params, result interface{}) (ID, error) + Call(ctx context.Context, method string, params, result any) (ID, error) // Notify invokes the target method but does not wait for a response. // The params will be marshaled to JSON before sending over the wire, and will // be handed to the method invoked. - Notify(ctx context.Context, method string, params interface{}) error + Notify(ctx context.Context, method string, params any) error // Go starts a goroutine to handle the connection. // It must be called exactly once for each Conn. @@ -76,7 +76,7 @@ func NewConn(s Stream) Conn { return conn } -func (c *conn) Notify(ctx context.Context, method string, params interface{}) (err error) { +func (c *conn) Notify(ctx context.Context, method string, params any) (err error) { notify, err := NewNotification(method, params) if err != nil { return fmt.Errorf("marshaling notify parameters: %v", err) @@ -96,7 +96,7 @@ func (c *conn) Notify(ctx context.Context, method string, params interface{}) (e return err } -func (c *conn) Call(ctx context.Context, method string, params, result interface{}) (_ ID, err error) { +func (c *conn) Call(ctx context.Context, method string, params, result any) (_ ID, err error) { // generate a new request identifier id := ID{number: atomic.AddInt64(&c.seq, 1)} call, err := NewCall(id, method, params) @@ -153,7 +153,7 @@ func (c *conn) Call(ctx context.Context, method string, params, result interface } func (c *conn) replier(req Request, spanDone func()) Replier { - return func(ctx context.Context, result interface{}, err error) error { + return func(ctx context.Context, result any, err error) error { defer func() { recordStatus(ctx, err) spanDone() diff --git a/internal/jsonrpc2/handler.go b/internal/jsonrpc2/handler.go index 27cb108922a..317b94f8ac1 100644 --- a/internal/jsonrpc2/handler.go +++ b/internal/jsonrpc2/handler.go @@ -18,7 +18,7 @@ type Handler func(ctx context.Context, reply Replier, req Request) error // Replier is passed to handlers to allow them to reply to the request. // If err is set then result will be ignored. -type Replier func(ctx context.Context, result interface{}, err error) error +type Replier func(ctx context.Context, result any, err error) error // MethodNotFound is a Handler that replies to all call requests with the // standard method not found response. @@ -32,7 +32,7 @@ func MethodNotFound(ctx context.Context, reply Replier, req Request) error { func MustReplyHandler(handler Handler) Handler { return func(ctx context.Context, reply Replier, req Request) error { called := false - err := handler(ctx, func(ctx context.Context, result interface{}, err error) error { + err := handler(ctx, func(ctx context.Context, result any, err error) error { if called { panic(fmt.Errorf("request %q replied to more than once", req.Method())) } @@ -59,7 +59,7 @@ func CancelHandler(handler Handler) (Handler, func(id ID)) { handling[call.ID()] = cancel mu.Unlock() innerReply := reply - reply = func(ctx context.Context, result interface{}, err error) error { + reply = func(ctx context.Context, result any, err error) error { mu.Lock() delete(handling, call.ID()) mu.Unlock() @@ -92,7 +92,7 @@ func AsyncHandler(handler Handler) Handler { nextRequest = make(chan struct{}) releaser := &releaser{ch: nextRequest} innerReply := reply - reply = func(ctx context.Context, result interface{}, err error) error { + reply = func(ctx context.Context, result any, err error) error { releaser.release(true) return innerReply(ctx, result, err) } diff --git a/internal/jsonrpc2/jsonrpc2_test.go b/internal/jsonrpc2/jsonrpc2_test.go index f62977edfce..b7688bc2334 100644 --- a/internal/jsonrpc2/jsonrpc2_test.go +++ b/internal/jsonrpc2/jsonrpc2_test.go @@ -23,8 +23,8 @@ var logRPC = flag.Bool("logrpc", false, "Enable jsonrpc2 communication logging") type callTest struct { method string - params interface{} - expect interface{} + params any + expect any } var callTests = []callTest{ @@ -35,10 +35,10 @@ var callTests = []callTest{ //TODO: expand the test cases } -func (test *callTest) newResults() interface{} { +func (test *callTest) newResults() any { switch e := test.expect.(type) { - case []interface{}: - var r []interface{} + case []any: + var r []any for _, v := range e { r = append(r, reflect.New(reflect.TypeOf(v)).Interface()) } @@ -50,7 +50,7 @@ func (test *callTest) newResults() interface{} { } } -func (test *callTest) verifyResults(t *testing.T, results interface{}) { +func (test *callTest) verifyResults(t *testing.T, results any) { if results == nil { return } diff --git a/internal/jsonrpc2/messages.go b/internal/jsonrpc2/messages.go index e87d772f398..5078b88f4ae 100644 --- a/internal/jsonrpc2/messages.go +++ b/internal/jsonrpc2/messages.go @@ -65,7 +65,7 @@ type Response struct { // NewNotification constructs a new Notification message for the supplied // method and parameters. -func NewNotification(method string, params interface{}) (*Notification, error) { +func NewNotification(method string, params any) (*Notification, error) { p, merr := marshalToRaw(params) return &Notification{method: method, params: p}, merr } @@ -98,7 +98,7 @@ func (n *Notification) UnmarshalJSON(data []byte) error { // NewCall constructs a new Call message for the supplied ID, method and // parameters. -func NewCall(id ID, method string, params interface{}) (*Call, error) { +func NewCall(id ID, method string, params any) (*Call, error) { p, merr := marshalToRaw(params) return &Call{id: id, method: method, params: p}, merr } @@ -135,7 +135,7 @@ func (c *Call) UnmarshalJSON(data []byte) error { // NewResponse constructs a new Response message that is a reply to the // supplied. If err is set result may be ignored. -func NewResponse(id ID, result interface{}, err error) (*Response, error) { +func NewResponse(id ID, result any, err error) (*Response, error) { r, merr := marshalToRaw(result) return &Response{id: id, result: r, err: err}, merr } @@ -229,7 +229,7 @@ func DecodeMessage(data []byte) (Message, error) { return call, nil } -func marshalToRaw(obj interface{}) (json.RawMessage, error) { +func marshalToRaw(obj any) (json.RawMessage, error) { data, err := json.Marshal(obj) if err != nil { return json.RawMessage{}, err diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index df885bfa4c3..4c52a1fd34b 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -260,7 +260,7 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde // Notify invokes the target method but does not wait for a response. // The params will be marshaled to JSON before sending over the wire, and will // be handed to the method invoked. -func (c *Connection) Notify(ctx context.Context, method string, params interface{}) (err error) { +func (c *Connection) Notify(ctx context.Context, method string, params any) (err error) { ctx, done := event.Start(ctx, method, jsonrpc2.Method.Of(method), jsonrpc2.RPCDirection.Of(jsonrpc2.Outbound), @@ -309,7 +309,7 @@ func (c *Connection) Notify(ctx context.Context, method string, params interface // be handed to the method invoked. // You do not have to wait for the response, it can just be ignored if not needed. // If sending the call failed, the response will be ready and have the error in it. -func (c *Connection) Call(ctx context.Context, method string, params interface{}) *AsyncCall { +func (c *Connection) Call(ctx context.Context, method string, params any) *AsyncCall { // Generate a new request identifier. id := Int64ID(atomic.AddInt64(&c.seq, 1)) ctx, endSpan := event.Start(ctx, method, @@ -410,7 +410,7 @@ func (ac *AsyncCall) retire(response *Response) { // Await waits for (and decodes) the results of a Call. // The response will be unmarshaled from JSON into the result. -func (ac *AsyncCall) Await(ctx context.Context, result interface{}) error { +func (ac *AsyncCall) Await(ctx context.Context, result any) error { select { case <-ctx.Done(): return ctx.Err() @@ -429,7 +429,7 @@ func (ac *AsyncCall) Await(ctx context.Context, result interface{}) error { // // Respond must be called exactly once for any message for which a handler // returns ErrAsyncResponse. It must not be called for any other message. -func (c *Connection) Respond(id ID, result interface{}, err error) error { +func (c *Connection) Respond(id ID, result any, err error) error { var req *incomingRequest c.updateInFlight(func(s *inFlightState) { req = s.incomingByID[id] @@ -678,7 +678,7 @@ func (c *Connection) handleAsync() { } // processResult processes the result of a request and, if appropriate, sends a response. -func (c *Connection) processResult(from interface{}, req *incomingRequest, result interface{}, err error) error { +func (c *Connection) processResult(from any, req *incomingRequest, result any, err error) error { switch err { case ErrAsyncResponse: if !req.IsCall() { @@ -781,7 +781,7 @@ func (c *Connection) write(ctx context.Context, msg Message) error { // internalErrorf reports an internal error. By default it panics, but if // c.onInternalError is non-nil it instead calls that and returns an error // wrapping ErrInternal. -func (c *Connection) internalErrorf(format string, args ...interface{}) error { +func (c *Connection) internalErrorf(format string, args ...any) error { err := fmt.Errorf(format, args...) if c.onInternalError == nil { panic("jsonrpc2: " + err.Error()) @@ -803,7 +803,7 @@ func labelStatus(ctx context.Context, err error) { // notDone is a context.Context wrapper that returns a nil Done channel. type notDone struct{ ctx context.Context } -func (ic notDone) Value(key interface{}) interface{} { +func (ic notDone) Value(key any) any { return ic.ctx.Value(key) } diff --git a/internal/jsonrpc2_v2/jsonrpc2.go b/internal/jsonrpc2_v2/jsonrpc2.go index 9d775de0603..270f4f341d8 100644 --- a/internal/jsonrpc2_v2/jsonrpc2.go +++ b/internal/jsonrpc2_v2/jsonrpc2.go @@ -44,13 +44,13 @@ type Preempter interface { // Otherwise, the result and error are processed as if returned by Handle. // // Preempt must not block. (The Context passed to it is for Values only.) - Preempt(ctx context.Context, req *Request) (result interface{}, err error) + Preempt(ctx context.Context, req *Request) (result any, err error) } // A PreempterFunc implements the Preempter interface for a standalone Preempt function. -type PreempterFunc func(ctx context.Context, req *Request) (interface{}, error) +type PreempterFunc func(ctx context.Context, req *Request) (any, error) -func (f PreempterFunc) Preempt(ctx context.Context, req *Request) (interface{}, error) { +func (f PreempterFunc) Preempt(ctx context.Context, req *Request) (any, error) { return f(ctx, req) } @@ -71,23 +71,23 @@ type Handler interface { // connection is broken or the request is canceled or completed. // (If Handle returns ErrAsyncResponse, ctx will remain uncanceled // until either Cancel or Respond is called for the request's ID.) - Handle(ctx context.Context, req *Request) (result interface{}, err error) + Handle(ctx context.Context, req *Request) (result any, err error) } type defaultHandler struct{} -func (defaultHandler) Preempt(context.Context, *Request) (interface{}, error) { +func (defaultHandler) Preempt(context.Context, *Request) (any, error) { return nil, ErrNotHandled } -func (defaultHandler) Handle(context.Context, *Request) (interface{}, error) { +func (defaultHandler) Handle(context.Context, *Request) (any, error) { return nil, ErrNotHandled } // A HandlerFunc implements the Handler interface for a standalone Handle function. -type HandlerFunc func(ctx context.Context, req *Request) (interface{}, error) +type HandlerFunc func(ctx context.Context, req *Request) (any, error) -func (f HandlerFunc) Handle(ctx context.Context, req *Request) (interface{}, error) { +func (f HandlerFunc) Handle(ctx context.Context, req *Request) (any, error) { return f(ctx, req) } diff --git a/internal/jsonrpc2_v2/jsonrpc2_test.go b/internal/jsonrpc2_v2/jsonrpc2_test.go index d75a20739e8..e42f63736c0 100644 --- a/internal/jsonrpc2_v2/jsonrpc2_test.go +++ b/internal/jsonrpc2_v2/jsonrpc2_test.go @@ -87,24 +87,24 @@ type invoker interface { type notify struct { method string - params interface{} + params any } type call struct { method string - params interface{} - expect interface{} + params any + expect any } type async struct { name string method string - params interface{} + params any } type collect struct { name string - expect interface{} + expect any fails bool } @@ -180,7 +180,7 @@ func (test call) Invoke(t *testing.T, ctx context.Context, h *handler) { func (test echo) Invoke(t *testing.T, ctx context.Context, h *handler) { results := newResults(test.expect) - if err := h.conn.Call(ctx, "echo", []interface{}{test.method, test.params}).Await(ctx, results); err != nil { + if err := h.conn.Call(ctx, "echo", []any{test.method, test.params}).Await(ctx, results); err != nil { t.Fatalf("%v:Echo failed: %v", test.method, err) } verifyResults(t, test.method, results, test.expect) @@ -221,10 +221,10 @@ func (test sequence) Invoke(t *testing.T, ctx context.Context, h *handler) { } // newResults makes a new empty copy of the expected type to put the results into -func newResults(expect interface{}) interface{} { +func newResults(expect any) any { switch e := expect.(type) { - case []interface{}: - var r []interface{} + case []any: + var r []any for _, v := range e { r = append(r, reflect.New(reflect.TypeOf(v)).Interface()) } @@ -237,7 +237,7 @@ func newResults(expect interface{}) interface{} { } // verifyResults compares the results to the expected values -func verifyResults(t *testing.T, method string, results interface{}, expect interface{}) { +func verifyResults(t *testing.T, method string, results any, expect any) { if expect == nil { if results != nil { t.Errorf("%v:Got results %+v where none expected", method, expect) @@ -278,7 +278,7 @@ func (h *handler) waiter(name string) chan struct{} { return waiter } -func (h *handler) Preempt(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) { +func (h *handler) Preempt(ctx context.Context, req *jsonrpc2.Request) (any, error) { switch req.Method { case "unblock": var name string @@ -304,7 +304,7 @@ func (h *handler) Preempt(ctx context.Context, req *jsonrpc2.Request) (interface } } -func (h *handler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) { +func (h *handler) Handle(ctx context.Context, req *jsonrpc2.Request) (any, error) { switch req.Method { case "no_args": if len(req.Params) > 0 { @@ -349,11 +349,11 @@ func (h *handler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{ } return path.Join(v...), nil case "echo": - var v []interface{} + var v []any if err := json.Unmarshal(req.Params, &v); err != nil { return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err) } - var result interface{} + var result any err := h.conn.Call(ctx, v[0].(string), v[1]).Await(ctx, &result) return result, err case "wait": diff --git a/internal/jsonrpc2_v2/messages.go b/internal/jsonrpc2_v2/messages.go index f02b879c3f2..9cfe6e70fe5 100644 --- a/internal/jsonrpc2_v2/messages.go +++ b/internal/jsonrpc2_v2/messages.go @@ -12,7 +12,7 @@ import ( // ID is a Request identifier. type ID struct { - value interface{} + value any } // Message is the interface to all jsonrpc2 message types. @@ -59,18 +59,18 @@ func Int64ID(i int64) ID { return ID{value: i} } func (id ID) IsValid() bool { return id.value != nil } // Raw returns the underlying value of the ID. -func (id ID) Raw() interface{} { return id.value } +func (id ID) Raw() any { return id.value } // NewNotification constructs a new Notification message for the supplied // method and parameters. -func NewNotification(method string, params interface{}) (*Request, error) { +func NewNotification(method string, params any) (*Request, error) { p, merr := marshalToRaw(params) return &Request{Method: method, Params: p}, merr } // NewCall constructs a new Call message for the supplied ID, method and // parameters. -func NewCall(id ID, method string, params interface{}) (*Request, error) { +func NewCall(id ID, method string, params any) (*Request, error) { p, merr := marshalToRaw(params) return &Request{ID: id, Method: method, Params: p}, merr } @@ -85,7 +85,7 @@ func (msg *Request) marshal(to *wireCombined) { // NewResponse constructs a new Response message that is a reply to the // supplied. If err is set result may be ignored. -func NewResponse(id ID, result interface{}, rerr error) (*Response, error) { +func NewResponse(id ID, result any, rerr error) (*Response, error) { r, merr := marshalToRaw(result) return &Response{ID: id, Result: r, Error: rerr}, merr } @@ -169,7 +169,7 @@ func DecodeMessage(data []byte) (Message, error) { return resp, nil } -func marshalToRaw(obj interface{}) (json.RawMessage, error) { +func marshalToRaw(obj any) (json.RawMessage, error) { if obj == nil { return nil, nil } diff --git a/internal/jsonrpc2_v2/serve_test.go b/internal/jsonrpc2_v2/serve_test.go index c5c41e201cd..8eb572c9d01 100644 --- a/internal/jsonrpc2_v2/serve_test.go +++ b/internal/jsonrpc2_v2/serve_test.go @@ -148,7 +148,7 @@ type msg struct { type fakeHandler struct{} -func (fakeHandler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) { +func (fakeHandler) Handle(ctx context.Context, req *jsonrpc2.Request) (any, error) { switch req.Method { case "ping": return &msg{"pong"}, nil @@ -296,7 +296,7 @@ func TestCloseCallRace(t *testing.T) { pokec := make(chan *jsonrpc2.AsyncCall, 1) s := jsonrpc2.NewServer(ctx, listener, jsonrpc2.BinderFunc(func(_ context.Context, srvConn *jsonrpc2.Connection) jsonrpc2.ConnectionOptions { - h := jsonrpc2.HandlerFunc(func(ctx context.Context, _ *jsonrpc2.Request) (interface{}, error) { + h := jsonrpc2.HandlerFunc(func(ctx context.Context, _ *jsonrpc2.Request) (any, error) { // Start a concurrent call from the server to the client. // The point of this test is to ensure this doesn't deadlock // if the client shuts down the connection concurrently. diff --git a/internal/jsonrpc2_v2/wire.go b/internal/jsonrpc2_v2/wire.go index 8f60fc62766..bc56951b5c3 100644 --- a/internal/jsonrpc2_v2/wire.go +++ b/internal/jsonrpc2_v2/wire.go @@ -45,7 +45,7 @@ const wireVersion = "2.0" // We can decode this and then work out which it is. type wireCombined struct { VersionTag string `json:"jsonrpc"` - ID interface{} `json:"id,omitempty"` + ID any `json:"id,omitempty"` Method string `json:"method,omitempty"` Params json.RawMessage `json:"params,omitempty"` Result json.RawMessage `json:"result,omitempty"` diff --git a/internal/jsonrpc2_v2/wire_test.go b/internal/jsonrpc2_v2/wire_test.go index e9337373239..c155c92f287 100644 --- a/internal/jsonrpc2_v2/wire_test.go +++ b/internal/jsonrpc2_v2/wire_test.go @@ -63,7 +63,7 @@ func TestWireMessage(t *testing.T) { } } -func newNotification(method string, params interface{}) jsonrpc2.Message { +func newNotification(method string, params any) jsonrpc2.Message { msg, err := jsonrpc2.NewNotification(method, params) if err != nil { panic(err) @@ -71,7 +71,7 @@ func newNotification(method string, params interface{}) jsonrpc2.Message { return msg } -func newID(id interface{}) jsonrpc2.ID { +func newID(id any) jsonrpc2.ID { switch v := id.(type) { case nil: return jsonrpc2.ID{} @@ -86,7 +86,7 @@ func newID(id interface{}) jsonrpc2.ID { } } -func newCall(id interface{}, method string, params interface{}) jsonrpc2.Message { +func newCall(id any, method string, params any) jsonrpc2.Message { msg, err := jsonrpc2.NewCall(newID(id), method, params) if err != nil { panic(err) @@ -94,7 +94,7 @@ func newCall(id interface{}, method string, params interface{}) jsonrpc2.Message return msg } -func newResponse(id interface{}, result interface{}, rerr error) jsonrpc2.Message { +func newResponse(id any, result any, rerr error) jsonrpc2.Message { msg, err := jsonrpc2.NewResponse(newID(id), result, rerr) if err != nil { panic(err) diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index e56af3bb45b..e49942a8827 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -42,7 +42,7 @@ import ( // The main purpose of the argument is to avoid the Function closure // needing to retain large objects (in practice: the snapshot) in // memory that can be supplied at call time by any caller. -type Function func(ctx context.Context, arg interface{}) interface{} +type Function func(ctx context.Context, arg any) any // A RefCounted is a value whose functional lifetime is determined by // reference counting. @@ -94,7 +94,7 @@ type Promise struct { // the function that will be used to populate the value function Function // value is set in completed state. - value interface{} + value any } // NewPromise returns a promise for the future result of calling the @@ -124,7 +124,7 @@ const ( // // It will never cause the value to be generated. // It will return the cached value, if present. -func (p *Promise) Cached() interface{} { +func (p *Promise) Cached() any { p.mu.Lock() defer p.mu.Unlock() if p.state == stateCompleted { @@ -144,7 +144,7 @@ func (p *Promise) Cached() interface{} { // If all concurrent calls to Get are cancelled, the context provided // to the function is cancelled. A later call to Get may attempt to // call the function again. -func (p *Promise) Get(ctx context.Context, arg interface{}) (interface{}, error) { +func (p *Promise) Get(ctx context.Context, arg any) (any, error) { if ctx.Err() != nil { return nil, ctx.Err() } @@ -163,7 +163,7 @@ func (p *Promise) Get(ctx context.Context, arg interface{}) (interface{}, error) } // run starts p.function and returns the result. p.mu must be locked. -func (p *Promise) run(ctx context.Context, arg interface{}) (interface{}, error) { +func (p *Promise) run(ctx context.Context, arg any) (any, error) { childCtx, cancel := context.WithCancel(xcontext.Detach(ctx)) p.cancel = cancel p.state = stateRunning @@ -210,7 +210,7 @@ func (p *Promise) run(ctx context.Context, arg interface{}) (interface{}, error) } // wait waits for the value to be computed, or ctx to be cancelled. p.mu must be locked. -func (p *Promise) wait(ctx context.Context) (interface{}, error) { +func (p *Promise) wait(ctx context.Context) (any, error) { p.waiters++ done := p.done p.mu.Unlock() @@ -258,7 +258,7 @@ type Store struct { evictionPolicy EvictionPolicy promisesMu sync.Mutex - promises map[interface{}]*Promise + promises map[any]*Promise } // NewStore creates a new store with the given eviction policy. @@ -276,13 +276,13 @@ func NewStore(policy EvictionPolicy) *Store { // // Once the last reference has been released, the promise is removed from the // store. -func (store *Store) Promise(key interface{}, function Function) (*Promise, func()) { +func (store *Store) Promise(key any, function Function) (*Promise, func()) { store.promisesMu.Lock() p, ok := store.promises[key] if !ok { p = NewPromise(reflect.TypeOf(key).String(), function) if store.promises == nil { - store.promises = map[interface{}]*Promise{} + store.promises = map[any]*Promise{} } store.promises[key] = p } @@ -323,7 +323,7 @@ func (s *Store) Stats() map[reflect.Type]int { // DebugOnlyIterate iterates through the store and, for each completed // promise, calls f(k, v) for the map key k and function result v. It // should only be used for debugging purposes. -func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { +func (s *Store) DebugOnlyIterate(f func(k, v any)) { s.promisesMu.Lock() defer s.promisesMu.Unlock() diff --git a/internal/memoize/memoize_test.go b/internal/memoize/memoize_test.go index c54572d59ca..08b097eb081 100644 --- a/internal/memoize/memoize_test.go +++ b/internal/memoize/memoize_test.go @@ -18,7 +18,7 @@ func TestGet(t *testing.T) { evaled := 0 - h, release := store.Promise("key", func(context.Context, interface{}) interface{} { + h, release := store.Promise("key", func(context.Context, any) any { evaled++ return "res" }) @@ -30,7 +30,7 @@ func TestGet(t *testing.T) { } } -func expectGet(t *testing.T, h *memoize.Promise, wantV interface{}) { +func expectGet(t *testing.T, h *memoize.Promise, wantV any) { t.Helper() gotV, gotErr := h.Get(context.Background(), nil) if gotV != wantV || gotErr != nil { @@ -40,7 +40,7 @@ func expectGet(t *testing.T, h *memoize.Promise, wantV interface{}) { func TestNewPromise(t *testing.T) { calls := 0 - f := func(context.Context, interface{}) interface{} { + f := func(context.Context, any) any { calls++ return calls } @@ -63,10 +63,10 @@ func TestStoredPromiseRefCounting(t *testing.T) { var store memoize.Store v1 := false v2 := false - p1, release1 := store.Promise("key1", func(context.Context, interface{}) interface{} { + p1, release1 := store.Promise("key1", func(context.Context, any) any { return &v1 }) - p2, release2 := store.Promise("key2", func(context.Context, interface{}) interface{} { + p2, release2 := store.Promise("key2", func(context.Context, any) any { return &v2 }) expectGet(t, p1, &v1) @@ -75,7 +75,7 @@ func TestStoredPromiseRefCounting(t *testing.T) { expectGet(t, p1, &v1) expectGet(t, p2, &v2) - p2Copy, release2Copy := store.Promise("key2", func(context.Context, interface{}) interface{} { + p2Copy, release2Copy := store.Promise("key2", func(context.Context, any) any { return &v1 }) if p2 != p2Copy { @@ -93,7 +93,7 @@ func TestStoredPromiseRefCounting(t *testing.T) { } release1() - p2Copy, release2Copy = store.Promise("key2", func(context.Context, interface{}) interface{} { + p2Copy, release2Copy = store.Promise("key2", func(context.Context, any) any { return &v2 }) if p2 == p2Copy { @@ -109,7 +109,7 @@ func TestPromiseDestroyedWhileRunning(t *testing.T) { c := make(chan int) var v int - h, release := store.Promise("key", func(ctx context.Context, _ interface{}) interface{} { + h, release := store.Promise("key", func(ctx context.Context, _ any) any { <-c <-c if err := ctx.Err(); err != nil { @@ -123,7 +123,7 @@ func TestPromiseDestroyedWhileRunning(t *testing.T) { var wg sync.WaitGroup wg.Add(1) - var got interface{} + var got any var err error go func() { got, err = h.Get(ctx, nil) @@ -146,7 +146,7 @@ func TestPromiseDestroyedWhileRunning(t *testing.T) { func TestDoubleReleasePanics(t *testing.T) { var store memoize.Store - _, release := store.Promise("key", func(ctx context.Context, _ interface{}) interface{} { return 0 }) + _, release := store.Promise("key", func(ctx context.Context, _ any) any { return 0 }) panicked := false diff --git a/internal/packagesinternal/packages.go b/internal/packagesinternal/packages.go index 784605914e0..25ebab663ba 100644 --- a/internal/packagesinternal/packages.go +++ b/internal/packagesinternal/packages.go @@ -17,4 +17,4 @@ var TypecheckCgo int var DepsErrors int // must be set as a LoadMode to call GetDepsErrors var SetModFlag = func(config any, value string) {} -var SetModFile = func(config interface{}, value string) {} +var SetModFile = func(config any, value string) {} diff --git a/internal/packagestest/expect.go b/internal/packagestest/expect.go index e3e3509579d..a5f76f55686 100644 --- a/internal/packagestest/expect.go +++ b/internal/packagestest/expect.go @@ -72,7 +72,7 @@ const ( // // It is safe to call this repeatedly with different method sets, but it is // not safe to call it concurrently. -func (e *Exported) Expect(methods map[string]interface{}) error { +func (e *Exported) Expect(methods map[string]any) error { if err := e.getNotes(); err != nil { return err } @@ -98,7 +98,7 @@ func (e *Exported) Expect(methods map[string]interface{}) error { n = &expect.Note{ Pos: n.Pos, Name: markMethod, - Args: []interface{}{n.Name, n.Name}, + Args: []any{n.Name, n.Name}, } } mi, ok := ms[n.Name] @@ -222,7 +222,7 @@ func (e *Exported) getMarkers() error { } // set markers early so that we don't call getMarkers again from Expect e.markers = make(map[string]Range) - return e.Expect(map[string]interface{}{ + return e.Expect(map[string]any{ markMethod: e.Mark, }) } @@ -243,7 +243,7 @@ var ( // It takes the args remaining, and returns the args it did not consume. // This allows a converter to consume 0 args for well known types, or multiple // args for compound types. -type converter func(*expect.Note, []interface{}) (reflect.Value, []interface{}, error) +type converter func(*expect.Note, []any) (reflect.Value, []any, error) // method is used to track information about Invoke methods that is expensive to // calculate so that we can work it out once rather than per marker. @@ -259,19 +259,19 @@ type method struct { func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { switch { case pt == noteType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { return reflect.ValueOf(n), args, nil }, nil case pt == fsetType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { return reflect.ValueOf(e.ExpectFileSet), args, nil }, nil case pt == exportedType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { return reflect.ValueOf(e), args, nil }, nil case pt == posType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err @@ -279,7 +279,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { return reflect.ValueOf(r.Start), remains, nil }, nil case pt == positionType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err @@ -287,7 +287,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { return reflect.ValueOf(e.ExpectFileSet.Position(r.Start)), remains, nil }, nil case pt == rangeType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err @@ -295,7 +295,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { return reflect.ValueOf(r), remains, nil }, nil case pt == identifierType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -310,7 +310,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { }, nil case pt == regexType: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -323,7 +323,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { }, nil case pt.Kind() == reflect.String: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -339,7 +339,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } }, nil case pt.Kind() == reflect.Int64: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -353,7 +353,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } }, nil case pt.Kind() == reflect.Bool: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -366,7 +366,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { return reflect.ValueOf(b), args, nil }, nil case pt.Kind() == reflect.Slice: - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { converter, err := e.buildConverter(pt.Elem()) if err != nil { return reflect.Value{}, nil, err @@ -384,7 +384,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { }, nil default: if pt.Kind() == reflect.Interface && pt.NumMethod() == 0 { - return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { + return func(n *expect.Note, args []any) (reflect.Value, []any, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } @@ -395,7 +395,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } } -func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (Range, []interface{}, error) { +func (e *Exported) rangeConverter(n *expect.Note, args []any) (Range, []any, error) { tokFile := e.ExpectFileSet.File(n.Pos) if len(args) < 1 { return Range{}, nil, fmt.Errorf("missing argument") diff --git a/internal/packagestest/expect_test.go b/internal/packagestest/expect_test.go index d155f5fe9e2..4f148b4183e 100644 --- a/internal/packagestest/expect_test.go +++ b/internal/packagestest/expect_test.go @@ -19,7 +19,7 @@ func TestExpect(t *testing.T) { }}) defer exported.Cleanup() checkCount := 0 - if err := exported.Expect(map[string]interface{}{ + if err := exported.Expect(map[string]any{ "check": func(src, target token.Position) { checkCount++ }, diff --git a/internal/packagestest/export.go b/internal/packagestest/export.go index f8d10718c09..ce992e17a90 100644 --- a/internal/packagestest/export.go +++ b/internal/packagestest/export.go @@ -97,7 +97,7 @@ type Module struct { // The keys are the file fragment that follows the module name, the value can // be a string or byte slice, in which case it is the contents of the // file, otherwise it must be a Writer function. - Files map[string]interface{} + Files map[string]any // Overlay is the set of source file overlays for the module. // The keys are the file fragment as in the Files configuration. @@ -479,7 +479,7 @@ func GroupFilesByModules(root string) ([]Module, error) { primarymod := &Module{ Name: root, - Files: make(map[string]interface{}), + Files: make(map[string]any), Overlay: make(map[string][]byte), } mods := map[string]*Module{ @@ -569,7 +569,7 @@ func GroupFilesByModules(root string) ([]Module, error) { } mods[path] = &Module{ Name: filepath.ToSlash(module), - Files: make(map[string]interface{}), + Files: make(map[string]any), Overlay: make(map[string][]byte), } currentModule = path @@ -587,8 +587,8 @@ func GroupFilesByModules(root string) ([]Module, error) { // This is to enable the common case in tests where you have a full copy of the // package in your testdata. // This will panic if there is any kind of error trying to walk the file tree. -func MustCopyFileTree(root string) map[string]interface{} { - result := map[string]interface{}{} +func MustCopyFileTree(root string) map[string]any { + result := map[string]any{} if err := filepath.Walk(filepath.FromSlash(root), func(path string, info os.FileInfo, err error) error { if err != nil { return err diff --git a/internal/packagestest/export_test.go b/internal/packagestest/export_test.go index 6c074216fbe..fae8bd2d5ba 100644 --- a/internal/packagestest/export_test.go +++ b/internal/packagestest/export_test.go @@ -16,7 +16,7 @@ import ( var testdata = []packagestest.Module{{ Name: "golang.org/fake1", - Files: map[string]interface{}{ + Files: map[string]any{ "a.go": packagestest.Symlink("testdata/a.go"), // broken symlink "b.go": "invalid file contents", }, @@ -26,22 +26,22 @@ var testdata = []packagestest.Module{{ }, }, { Name: "golang.org/fake2", - Files: map[string]interface{}{ + Files: map[string]any{ "other/a.go": "package fake2", }, }, { Name: "golang.org/fake2/v2", - Files: map[string]interface{}{ + Files: map[string]any{ "other/a.go": "package fake2", }, }, { Name: "golang.org/fake3@v1.0.0", - Files: map[string]interface{}{ + Files: map[string]any{ "other/a.go": "package fake3", }, }, { Name: "golang.org/fake3@v1.1.0", - Files: map[string]interface{}{ + Files: map[string]any{ "other/a.go": "package fake3", }, }} @@ -97,13 +97,13 @@ func TestGroupFilesByModules(t *testing.T) { want: []packagestest.Module{ { Name: "testdata/groups/one", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": true, }, }, { Name: "example.com/extra", - Files: map[string]interface{}{ + Files: map[string]any{ "help.go": true, }, }, @@ -114,7 +114,7 @@ func TestGroupFilesByModules(t *testing.T) { want: []packagestest.Module{ { Name: "testdata/groups/two", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": true, "expect/yo.go": true, "expect/yo_test.go": true, @@ -122,33 +122,33 @@ func TestGroupFilesByModules(t *testing.T) { }, { Name: "example.com/extra", - Files: map[string]interface{}{ + Files: map[string]any{ "yo.go": true, "geez/help.go": true, }, }, { Name: "example.com/extra/v2", - Files: map[string]interface{}{ + Files: map[string]any{ "me.go": true, "geez/help.go": true, }, }, { Name: "example.com/tempmod", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": true, }, }, { Name: "example.com/what@v1.0.0", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": true, }, }, { Name: "example.com/what@v1.1.0", - Files: map[string]interface{}{ + Files: map[string]any{ "main.go": true, }, }, diff --git a/internal/tool/tool.go b/internal/tool/tool.go index fe2b1c289b8..6420c9667d9 100644 --- a/internal/tool/tool.go +++ b/internal/tool/tool.go @@ -81,7 +81,7 @@ func (e commandLineError) Error() string { return string(e) } // CommandLineErrorf is like fmt.Errorf except that it returns a value that // triggers printing of the command line help. // In general you should use this when generating command line validation errors. -func CommandLineErrorf(message string, args ...interface{}) error { +func CommandLineErrorf(message string, args ...any) error { return commandLineError(fmt.Sprintf(message, args...)) } diff --git a/internal/typeparams/normalize.go b/internal/typeparams/normalize.go index 93c80fdc96c..f49802b8ef7 100644 --- a/internal/typeparams/normalize.go +++ b/internal/typeparams/normalize.go @@ -120,7 +120,7 @@ type termSet struct { terms termlist } -func indentf(depth int, format string, args ...interface{}) { +func indentf(depth int, format string, args ...any) { fmt.Fprintf(os.Stderr, strings.Repeat(".", depth)+format+"\n", args...) } diff --git a/internal/xcontext/xcontext.go b/internal/xcontext/xcontext.go index ff8ed4ebb95..641dfe5a102 100644 --- a/internal/xcontext/xcontext.go +++ b/internal/xcontext/xcontext.go @@ -17,7 +17,7 @@ func Detach(ctx context.Context) context.Context { return detachedContext{ctx} } type detachedContext struct{ parent context.Context } -func (v detachedContext) Deadline() (time.Time, bool) { return time.Time{}, false } -func (v detachedContext) Done() <-chan struct{} { return nil } -func (v detachedContext) Err() error { return nil } -func (v detachedContext) Value(key interface{}) interface{} { return v.parent.Value(key) } +func (v detachedContext) Deadline() (time.Time, bool) { return time.Time{}, false } +func (v detachedContext) Done() <-chan struct{} { return nil } +func (v detachedContext) Err() error { return nil } +func (v detachedContext) Value(key any) any { return v.parent.Value(key) } From 8a39d47f70846bf278b4bfb793f04b76e478e37b Mon Sep 17 00:00:00 2001 From: Viktor Blomqvist Date: Mon, 13 Jan 2025 20:25:31 +0100 Subject: [PATCH 44/99] gopls/internal/golang: Add "Eliminate dot import" code action. The code action will qualify identifiers if possible. If there are names in scope which will shadow the package name then the code action fails. Updates golang/go#70319. Change-Id: I7c1ff1c60d592cb6f1093ab653c04a44d7092607 Reviewed-on: https://go-review.googlesource.com/c/tools/+/642016 Auto-Submit: Robert Findley Reviewed-by: Robert Findley Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/doc/features/transformation.md | 9 ++ gopls/doc/release/v0.19.0.md | 7 ++ gopls/internal/golang/codeaction.go | 100 ++++++++++++++++++ gopls/internal/settings/codeactionkind.go | 19 ++-- .../codeaction/eliminate_dot_import.txt | 40 +++++++ 5 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 gopls/doc/release/v0.19.0.md create mode 100644 gopls/internal/test/marker/testdata/codeaction/eliminate_dot_import.txt diff --git a/gopls/doc/features/transformation.md b/gopls/doc/features/transformation.md index caf13221cfa..a72ff676832 100644 --- a/gopls/doc/features/transformation.md +++ b/gopls/doc/features/transformation.md @@ -814,3 +814,12 @@ which HTML documents are composed: ![Before "Add cases for Addr"](../assets/fill-switch-enum-before.png) ![After "Add cases for Addr"](../assets/fill-switch-enum-after.png) + + + +### `refactor.rewrite.eliminateDotImport`: Eliminate dot import + +When the cursor is on a dot import gopls can offer the "Eliminate dot import" +code action, which removes the dot from the import and qualifies uses of the +package throughout the file. This code action is offered only if +each use of the package can be qualified without collisions with existing names. diff --git a/gopls/doc/release/v0.19.0.md b/gopls/doc/release/v0.19.0.md new file mode 100644 index 00000000000..0b3ea64c305 --- /dev/null +++ b/gopls/doc/release/v0.19.0.md @@ -0,0 +1,7 @@ +# New features + +## "Eliminate dot import" code action + +This code action, available on a dotted import, will offer to replace +the import with a regular one and qualify each use of the package +with its name. diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 49a861852ff..587ae3e2de3 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -260,6 +260,7 @@ var codeActionProducers = [...]codeActionProducer{ {kind: settings.RefactorRewriteMoveParamLeft, fn: refactorRewriteMoveParamLeft, needPkg: true}, {kind: settings.RefactorRewriteMoveParamRight, fn: refactorRewriteMoveParamRight, needPkg: true}, {kind: settings.RefactorRewriteSplitLines, fn: refactorRewriteSplitLines, needPkg: true}, + {kind: settings.RefactorRewriteEliminateDotImport, fn: refactorRewriteEliminateDotImport, needPkg: true}, // Note: don't forget to update the allow-list in Server.CodeAction // when adding new query operations like GoTest and GoDoc that @@ -678,6 +679,105 @@ func refactorRewriteSplitLines(ctx context.Context, req *codeActionsRequest) err return nil } +func refactorRewriteEliminateDotImport(ctx context.Context, req *codeActionsRequest) error { + // Figure out if the request is placed over a dot import. + var importSpec *ast.ImportSpec + for _, imp := range req.pgf.File.Imports { + if posRangeContains(imp.Pos(), imp.End(), req.start, req.end) { + importSpec = imp + break + } + } + if importSpec == nil { + return nil + } + if importSpec.Name == nil || importSpec.Name.Name != "." { + return nil + } + + // dotImported package path and its imported name after removing the dot. + imported := req.pkg.TypesInfo().PkgNameOf(importSpec).Imported() + newName := imported.Name() + + rng, err := req.pgf.PosRange(importSpec.Name.Pos(), importSpec.Path.Pos()) + if err != nil { + return err + } + // Delete the '.' part of the import. + edits := []protocol.TextEdit{{ + Range: rng, + }} + + fileScope, ok := req.pkg.TypesInfo().Scopes[req.pgf.File] + if !ok { + return nil + } + + // Go through each use of the dot imported package, checking its scope for + // shadowing and calculating an edit to qualify the identifier. + var stack []ast.Node + ast.Inspect(req.pgf.File, func(n ast.Node) bool { + if n == nil { + stack = stack[:len(stack)-1] // pop + return false + } + stack = append(stack, n) // push + + ident, ok := n.(*ast.Ident) + if !ok { + return true + } + // Only keep identifiers that use a symbol from the + // dot imported package. + use := req.pkg.TypesInfo().Uses[ident] + if use == nil || use.Pkg() == nil { + return true + } + if use.Pkg() != imported { + return true + } + + // Only qualify unqualified identifiers (due to dot imports). + // All other references to a symbol imported from another package + // are nested within a select expression (pkg.Foo, v.Method, v.Field). + if is[*ast.SelectorExpr](stack[len(stack)-2]) { + return true + } + + // Make sure that the package name will not be shadowed by something else in scope. + // If it is then we cannot offer this particular code action. + // + // TODO: If the object found in scope is the package imported without a + // dot, or some builtin not used in the file, the code action could be + // allowed to go through. + sc := fileScope.Innermost(ident.Pos()) + if sc == nil { + return true + } + _, obj := sc.LookupParent(newName, ident.Pos()) + if obj != nil { + return true + } + + rng, err := req.pgf.PosRange(ident.Pos(), ident.Pos()) // sic, zero-width range before ident + if err != nil { + return true + } + edits = append(edits, protocol.TextEdit{ + Range: rng, + NewText: newName + ".", + }) + + return true + }) + + req.addEditAction("Eliminate dot import", nil, protocol.DocumentChangeEdit( + req.fh, + edits, + )) + return nil +} + // refactorRewriteJoinLines produces "Join ITEMS into one line" code actions. // See [joinLines] for command implementation. func refactorRewriteJoinLines(ctx context.Context, req *codeActionsRequest) error { diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go index fcce7cd2682..09d9d419567 100644 --- a/gopls/internal/settings/codeactionkind.go +++ b/gopls/internal/settings/codeactionkind.go @@ -86,15 +86,16 @@ const ( GoplsDocFeatures protocol.CodeActionKind = "gopls.doc.features" // refactor.rewrite - RefactorRewriteChangeQuote protocol.CodeActionKind = "refactor.rewrite.changeQuote" - RefactorRewriteFillStruct protocol.CodeActionKind = "refactor.rewrite.fillStruct" - RefactorRewriteFillSwitch protocol.CodeActionKind = "refactor.rewrite.fillSwitch" - RefactorRewriteInvertIf protocol.CodeActionKind = "refactor.rewrite.invertIf" - RefactorRewriteJoinLines protocol.CodeActionKind = "refactor.rewrite.joinLines" - RefactorRewriteRemoveUnusedParam protocol.CodeActionKind = "refactor.rewrite.removeUnusedParam" - RefactorRewriteMoveParamLeft protocol.CodeActionKind = "refactor.rewrite.moveParamLeft" - RefactorRewriteMoveParamRight protocol.CodeActionKind = "refactor.rewrite.moveParamRight" - RefactorRewriteSplitLines protocol.CodeActionKind = "refactor.rewrite.splitLines" + RefactorRewriteChangeQuote protocol.CodeActionKind = "refactor.rewrite.changeQuote" + RefactorRewriteFillStruct protocol.CodeActionKind = "refactor.rewrite.fillStruct" + RefactorRewriteFillSwitch protocol.CodeActionKind = "refactor.rewrite.fillSwitch" + RefactorRewriteInvertIf protocol.CodeActionKind = "refactor.rewrite.invertIf" + RefactorRewriteJoinLines protocol.CodeActionKind = "refactor.rewrite.joinLines" + RefactorRewriteRemoveUnusedParam protocol.CodeActionKind = "refactor.rewrite.removeUnusedParam" + RefactorRewriteMoveParamLeft protocol.CodeActionKind = "refactor.rewrite.moveParamLeft" + RefactorRewriteMoveParamRight protocol.CodeActionKind = "refactor.rewrite.moveParamRight" + RefactorRewriteSplitLines protocol.CodeActionKind = "refactor.rewrite.splitLines" + RefactorRewriteEliminateDotImport protocol.CodeActionKind = "refactor.rewrite.eliminateDotImport" // refactor.inline RefactorInlineCall protocol.CodeActionKind = "refactor.inline.call" diff --git a/gopls/internal/test/marker/testdata/codeaction/eliminate_dot_import.txt b/gopls/internal/test/marker/testdata/codeaction/eliminate_dot_import.txt new file mode 100644 index 00000000000..e72d8bd5417 --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/eliminate_dot_import.txt @@ -0,0 +1,40 @@ +This test checks the behavior of the 'remove dot import' code action. + +-- go.mod -- +module golang.org/lsptests/removedotimport + +go 1.18 + +-- a.go -- +package dotimport + +// Base case: action is OK. + +import ( + . "fmt" //@codeaction(`.`, "refactor.rewrite.eliminateDotImport", edit=a1) + . "bytes" //@codeaction(`.`, "refactor.rewrite.eliminateDotImport", edit=a2) +) + +var _ = a + +func a() { + Println("hello") + + buf := NewBuffer(nil) + buf.Grow(10) +} + +-- @a1/a.go -- +@@ -6 +6 @@ +- . "fmt" //@codeaction(`.`, "refactor.rewrite.eliminateDotImport", edit=a1) ++ "fmt" //@codeaction(`.`, "refactor.rewrite.eliminateDotImport", edit=a1) +@@ -13 +13 @@ +- Println("hello") ++ fmt.Println("hello") +-- @a2/a.go -- +@@ -7 +7 @@ +- . "bytes" //@codeaction(`.`, "refactor.rewrite.eliminateDotImport", edit=a2) ++ "bytes" //@codeaction(`.`, "refactor.rewrite.eliminateDotImport", edit=a2) +@@ -15 +15 @@ +- buf := NewBuffer(nil) ++ buf := bytes.NewBuffer(nil) From 300465cc970af3836a5368d587764267a8f4d77e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 19 Feb 2025 18:33:23 -0500 Subject: [PATCH 45/99] gopls/internal/analysis/modernize: fix rangeint bug info.Defs[v] is nil if the loop variable is not declared (for i = 0 instead of for i := 0). + test Updates golang/go#71847 Change-Id: I28f82188e813f2d4f1ddc9335f0c13bd90c31ec1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/650815 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/analysis/modernize/rangeint.go | 2 +- .../modernize/testdata/src/rangeint/rangeint.go | 13 +++++++++++++ .../testdata/src/rangeint/rangeint.go.golden | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/gopls/internal/analysis/modernize/rangeint.go b/gopls/internal/analysis/modernize/rangeint.go index 2d25d6a0a06..273c13877bd 100644 --- a/gopls/internal/analysis/modernize/rangeint.go +++ b/gopls/internal/analysis/modernize/rangeint.go @@ -75,7 +75,7 @@ func rangeint(pass *analysis.Pass) { // Have: for i = 0; i < limit; i++ {} // Find references to i within the loop body. - v := info.Defs[index] + v := info.ObjectOf(index) used := false for curId := range curLoop.Child(loop.Body).Preorder((*ast.Ident)(nil)) { id := curId.Node().(*ast.Ident) diff --git a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go index a60bd5eac37..6c30f183340 100644 --- a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go +++ b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go @@ -12,6 +12,9 @@ func _(i int, s struct{ i int }, slice []int) { for i := 0; i < len(slice); i++ { // want "for loop can be modernized using range over int" println(slice[i]) } + for i := 0; i < len(""); i++ { // want "for loop can be modernized using range over int" + // NB: not simplified to range "" + } // nope for i := 0; i < 10; { // nope: missing increment @@ -38,3 +41,13 @@ func _(i int, s struct{ i int }, slice []int) { } func f() int { return 0 } + +// Repro for part of #71847: ("for range n is invalid if the loop body contains i++"): +func _(s string) { + var i int // (this is necessary) + for i = 0; i < len(s); i++ { // nope: loop body increments i + if true { + i++ // nope + } + } +} diff --git a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden index 348f77508ac..52f16347b1e 100644 --- a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden @@ -12,6 +12,9 @@ func _(i int, s struct{ i int }, slice []int) { for i := range slice { // want "for loop can be modernized using range over int" println(slice[i]) } + for range len("") { // want "for loop can be modernized using range over int" + // NB: not simplified to range "" + } // nope for i := 0; i < 10; { // nope: missing increment @@ -38,3 +41,13 @@ func _(i int, s struct{ i int }, slice []int) { } func f() int { return 0 } + +// Repro for part of #71847: ("for range n is invalid if the loop body contains i++"): +func _(s string) { + var i int // (this is necessary) + for i = 0; i < len(s); i++ { // nope: loop body increments i + if true { + i++ // nope + } + } +} From f0af81c3ddded0970b2ffe7922f269b53e1a63bb Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 14 Feb 2025 14:24:21 -0500 Subject: [PATCH 46/99] gopls/internal/goasm: support Definition in Go *.s assembly This CL provides a minimal implementation of the Definition query within Go assembly files, plus a test. For now it only works for references to package-level symbols in the same package or a dependency. Details: - add file.Kind Asm and protocol.LanguageKind "go.s". - include .s files in metadata.Graph.IDs mapping. - set LanguageKind correctly in gopls CLI. Also: - add String() method to file.Handle. - add convenient forward deps iterator to Graph. - internal/extract: extract notes from .s files too. Updates golang/go#71754 Change-Id: I0c518c3279f825411221ebe23dc04654e129fc56 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649461 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Reviewed-by: Robert Findley Commit-Queue: Alan Donovan --- gopls/internal/cache/fs_memoized.go | 2 + gopls/internal/cache/fs_overlay.go | 2 + gopls/internal/cache/metadata/graph.go | 36 +++++ gopls/internal/cache/parse_cache_test.go | 4 + gopls/internal/cache/session.go | 1 + gopls/internal/cache/snapshot.go | 21 +++ gopls/internal/cmd/cmd.go | 17 ++- gopls/internal/cmd/definition.go | 2 +- gopls/internal/file/file.go | 2 + gopls/internal/file/kind.go | 8 +- gopls/internal/goasm/definition.go | 125 ++++++++++++++++++ gopls/internal/golang/snapshot.go | 14 +- gopls/internal/server/definition.go | 3 + .../internal/test/integration/fake/editor.go | 14 +- .../test/marker/testdata/definition/asm.txt | 33 +++++ internal/expect/extract.go | 46 ++++++- 16 files changed, 304 insertions(+), 26 deletions(-) create mode 100644 gopls/internal/goasm/definition.go create mode 100644 gopls/internal/test/marker/testdata/definition/asm.txt diff --git a/gopls/internal/cache/fs_memoized.go b/gopls/internal/cache/fs_memoized.go index 9f156e3e153..a179b0ce7f5 100644 --- a/gopls/internal/cache/fs_memoized.go +++ b/gopls/internal/cache/fs_memoized.go @@ -41,6 +41,8 @@ type diskFile struct { err error } +func (h *diskFile) String() string { return h.uri.Path() } + func (h *diskFile) URI() protocol.DocumentURI { return h.uri } func (h *diskFile) Identity() file.Identity { diff --git a/gopls/internal/cache/fs_overlay.go b/gopls/internal/cache/fs_overlay.go index 265598bb967..b18d6d3f154 100644 --- a/gopls/internal/cache/fs_overlay.go +++ b/gopls/internal/cache/fs_overlay.go @@ -64,6 +64,8 @@ type overlay struct { saved bool } +func (o *overlay) String() string { return o.uri.Path() } + func (o *overlay) URI() protocol.DocumentURI { return o.uri } func (o *overlay) Identity() file.Identity { diff --git a/gopls/internal/cache/metadata/graph.go b/gopls/internal/cache/metadata/graph.go index 4b846df53be..716b767e37b 100644 --- a/gopls/internal/cache/metadata/graph.go +++ b/gopls/internal/cache/metadata/graph.go @@ -5,7 +5,9 @@ package metadata import ( + "iter" "sort" + "strings" "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/protocol" @@ -99,6 +101,11 @@ func newGraph(pkgs map[PackageID]*Package) *Graph { for _, uri := range mp.GoFiles { uris[uri] = struct{}{} } + for _, uri := range mp.OtherFiles { + if strings.HasSuffix(string(uri), ".s") { // assembly + uris[uri] = struct{}{} + } + } for uri := range uris { uriIDs[uri] = append(uriIDs[uri], id) } @@ -160,6 +167,35 @@ func (g *Graph) ReverseReflexiveTransitiveClosure(ids ...PackageID) map[PackageI return seen } +// ForwardReflexiveTransitiveClosure returns an iterator over the +// specified nodes and all their forward dependencies, in an arbitrary +// topological (dependencies-first) order. The order may vary. +func (g *Graph) ForwardReflexiveTransitiveClosure(ids ...PackageID) iter.Seq[*Package] { + return func(yield func(*Package) bool) { + seen := make(map[PackageID]bool) + var visit func(PackageID) bool + visit = func(id PackageID) bool { + if !seen[id] { + seen[id] = true + if mp := g.Packages[id]; mp != nil { + for _, depID := range mp.DepsByPkgPath { + if !visit(depID) { + return false + } + } + if !yield(mp) { + return false + } + } + } + return true + } + for _, id := range ids { + visit(id) + } + } +} + // breakImportCycles breaks import cycles in the metadata by deleting // Deps* edges. It modifies only metadata present in the 'updates' // subset. This function has an internal test. diff --git a/gopls/internal/cache/parse_cache_test.go b/gopls/internal/cache/parse_cache_test.go index 7aefac77c38..fe0548aa20d 100644 --- a/gopls/internal/cache/parse_cache_test.go +++ b/gopls/internal/cache/parse_cache_test.go @@ -218,6 +218,10 @@ type fakeFileHandle struct { hash file.Hash } +func (h fakeFileHandle) String() string { + return h.uri.Path() +} + func (h fakeFileHandle) URI() protocol.DocumentURI { return h.uri } diff --git a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go index 5ae753eb91c..c2f57e985f7 100644 --- a/gopls/internal/cache/session.go +++ b/gopls/internal/cache/session.go @@ -1084,6 +1084,7 @@ type brokenFile struct { err error } +func (b brokenFile) String() string { return b.uri.Path() } func (b brokenFile) URI() protocol.DocumentURI { return b.uri } func (b brokenFile) Identity() file.Identity { return file.Identity{URI: b.uri} } func (b brokenFile) SameContentsOnDisk() bool { return false } diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index 578cea61eb7..754389c7008 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -323,6 +323,8 @@ func fileKind(fh file.Handle) file.Kind { return file.Sum case ".work": return file.Work + case ".s": + return file.Asm } return file.UnknownKind } @@ -645,6 +647,21 @@ func (s *Snapshot) Tests(ctx context.Context, ids ...PackageID) ([]*testfuncs.In return indexes, s.forEachPackage(ctx, ids, pre, post) } +// NarrowestMetadataForFile returns metadata for the narrowest package +// (the one with the fewest files) that encloses the specified file. +// The result may be a test variant, but never an intermediate test variant. +func (snapshot *Snapshot) NarrowestMetadataForFile(ctx context.Context, uri protocol.DocumentURI) (*metadata.Package, error) { + mps, err := snapshot.MetadataForFile(ctx, uri) + if err != nil { + return nil, err + } + metadata.RemoveIntermediateTestVariants(&mps) + if len(mps) == 0 { + return nil, fmt.Errorf("no package metadata for file %s", uri) + } + return mps[0], nil +} + // MetadataForFile returns a new slice containing metadata for each // package containing the Go file identified by uri, ordered by the // number of CompiledGoFiles (i.e. "narrowest" to "widest" package), @@ -652,6 +669,10 @@ func (s *Snapshot) Tests(ctx context.Context, ids ...PackageID) ([]*testfuncs.In // The result may include tests and intermediate test variants of // importable packages. // It returns an error if the context was cancelled. +// +// TODO(adonovan): in nearly all cases the caller must use +// [metadata.RemoveIntermediateTestVariants]. Make this a parameter to +// force the caller to consider it (and reduce code). func (s *Snapshot) MetadataForFile(ctx context.Context, uri protocol.DocumentURI) ([]*metadata.Package, error) { if s.view.typ == AdHocView { // As described in golang/go#57209, in ad-hoc workspaces (where we load ./ diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go index 2a161ad0fc8..8bd7d7b899f 100644 --- a/gopls/internal/cmd/cmd.go +++ b/gopls/internal/cmd/cmd.go @@ -773,10 +773,25 @@ func (c *connection) openFile(ctx context.Context, uri protocol.DocumentURI) (*c return nil, file.err } + // Choose language ID from file extension. + var langID protocol.LanguageKind // "" eventually maps to file.UnknownKind + switch filepath.Ext(uri.Path()) { + case ".go": + langID = "go" + case ".mod": + langID = "go.mod" + case ".sum": + langID = "go.sum" + case ".work": + langID = "go.work" + case ".s": + langID = "go.s" + } + p := &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{ URI: uri, - LanguageID: "go", + LanguageID: langID, Version: 1, Text: string(file.mapper.Content), }, diff --git a/gopls/internal/cmd/definition.go b/gopls/internal/cmd/definition.go index d9cd98554e3..71e8b1511bd 100644 --- a/gopls/internal/cmd/definition.go +++ b/gopls/internal/cmd/definition.go @@ -96,7 +96,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error { } if len(locs) == 0 { - return fmt.Errorf("%v: not an identifier", from) + return fmt.Errorf("%v: no definition location (not an identifier?)", from) } file, err = conn.openFile(ctx, locs[0].URI) if err != nil { diff --git a/gopls/internal/file/file.go b/gopls/internal/file/file.go index 5f8be06cf60..b817306aa07 100644 --- a/gopls/internal/file/file.go +++ b/gopls/internal/file/file.go @@ -49,6 +49,8 @@ type Handle interface { // Content returns the contents of a file. // If the file is not available, returns a nil slice and an error. Content() ([]byte, error) + // String returns the file's path. + String() string } // A Source maps URIs to Handles. diff --git a/gopls/internal/file/kind.go b/gopls/internal/file/kind.go index 087a57f32d0..6a0ed009ed5 100644 --- a/gopls/internal/file/kind.go +++ b/gopls/internal/file/kind.go @@ -28,6 +28,8 @@ const ( Tmpl // Work is a go.work file. Work + // Asm is a Go assembly (.s) file. + Asm ) func (k Kind) String() string { @@ -42,13 +44,15 @@ func (k Kind) String() string { return "tmpl" case Work: return "go.work" + case Asm: + return "Go assembly" default: return fmt.Sprintf("internal error: unknown file kind %d", k) } } // KindForLang returns the gopls file [Kind] associated with the given LSP -// LanguageKind string from protocol.TextDocumentItem.LanguageID, +// LanguageKind string from the LanguageID field of [protocol.TextDocumentItem], // or UnknownKind if the language is not one recognized by gopls. func KindForLang(langID protocol.LanguageKind) Kind { switch langID { @@ -62,6 +66,8 @@ func KindForLang(langID protocol.LanguageKind) Kind { return Tmpl case "go.work": return Work + case "go.s": + return Asm default: return UnknownKind } diff --git a/gopls/internal/goasm/definition.go b/gopls/internal/goasm/definition.go new file mode 100644 index 00000000000..4403e7cac7f --- /dev/null +++ b/gopls/internal/goasm/definition.go @@ -0,0 +1,125 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goasm + +import ( + "bytes" + "context" + "fmt" + "go/token" + "strings" + "unicode" + + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/morestrings" + "golang.org/x/tools/internal/event" +) + +// Definition handles the textDocument/definition request for Go assembly files. +func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) { + ctx, done := event.Start(ctx, "goasm.Definition") + defer done() + + mp, err := snapshot.NarrowestMetadataForFile(ctx, fh.URI()) + if err != nil { + return nil, err + } + + // Read the file. + content, err := fh.Content() + if err != nil { + return nil, err + } + mapper := protocol.NewMapper(fh.URI(), content) + offset, err := mapper.PositionOffset(position) + if err != nil { + return nil, err + } + + // Figure out the selected symbol. + // For now, just find the identifier around the cursor. + // + // TODO(adonovan): use a real asm parser; see cmd/asm/internal/asm/parse.go. + // Ideally this would just be just another attribute of the + // type-checked cache.Package. + nonIdentRune := func(r rune) bool { return !isIdentRune(r) } + i := bytes.LastIndexFunc(content[:offset], nonIdentRune) + j := bytes.IndexFunc(content[offset:], nonIdentRune) + if j < 0 || j == 0 { + return nil, nil // identifier runs to EOF, or not an identifier + } + sym := string(content[i+1 : offset+j]) + sym = strings.ReplaceAll(sym, "·", ".") // (U+00B7 MIDDLE DOT) + sym = strings.ReplaceAll(sym, "∕", "/") // (U+2215 DIVISION SLASH) + if sym != "" && sym[0] == '.' { + sym = string(mp.PkgPath) + sym + } + + // package-qualified symbol? + if pkgpath, name, ok := morestrings.CutLast(sym, "."); ok { + // Find declaring package among dependencies. + // + // TODO(adonovan): assembly may legally reference + // non-dependencies. For example, sync/atomic calls + // internal/runtime/atomic. Perhaps we should search + // the entire metadata graph, but that's path-dependent. + var declaring *metadata.Package + for pkg := range snapshot.MetadataGraph().ForwardReflexiveTransitiveClosure(mp.ID) { + if pkg.PkgPath == metadata.PackagePath(pkgpath) { + declaring = pkg + break + } + } + if declaring == nil { + return nil, fmt.Errorf("package %q is not a dependency", pkgpath) + } + + pkgs, err := snapshot.TypeCheck(ctx, declaring.ID) + if err != nil { + return nil, err + } + pkg := pkgs[0] + def := pkg.Types().Scope().Lookup(name) + if def == nil { + return nil, fmt.Errorf("no symbol %q in package %q", name, pkgpath) + } + loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, def.Pos(), def.Pos()) + if err == nil { + return []protocol.Location{loc}, nil + } + } + + // TODO(adonovan): support jump to var, block label, and other + // TEXT, DATA, and GLOBAL symbols in the same file. Needs asm parser. + + return nil, nil +} + +// The assembler allows center dot (· U+00B7) and +// division slash (∕ U+2215) to work as identifier characters. +func isIdentRune(r rune) bool { + return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '·' || r == '∕' +} + +// TODO(rfindley): avoid the duplicate column mapping here, by associating a +// column mapper with each file handle. +// TODO(adonovan): plundered from ../golang; factor. +func mapPosition(ctx context.Context, fset *token.FileSet, s file.Source, start, end token.Pos) (protocol.Location, error) { + file := fset.File(start) + uri := protocol.URIFromPath(file.Name()) + fh, err := s.ReadFile(ctx, uri) + if err != nil { + return protocol.Location{}, err + } + content, err := fh.Content() + if err != nil { + return protocol.Location{}, err + } + m := protocol.NewMapper(fh.URI(), content) + return m.PosLocation(file, start, end) +} diff --git a/gopls/internal/golang/snapshot.go b/gopls/internal/golang/snapshot.go index c381c962d08..30199d45463 100644 --- a/gopls/internal/golang/snapshot.go +++ b/gopls/internal/golang/snapshot.go @@ -14,19 +14,9 @@ import ( "golang.org/x/tools/gopls/internal/protocol" ) -// NarrowestMetadataForFile returns metadata for the narrowest package -// (the one with the fewest files) that encloses the specified file. -// The result may be a test variant, but never an intermediate test variant. +//go:fix inline func NarrowestMetadataForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*metadata.Package, error) { - mps, err := snapshot.MetadataForFile(ctx, uri) - if err != nil { - return nil, err - } - metadata.RemoveIntermediateTestVariants(&mps) - if len(mps) == 0 { - return nil, fmt.Errorf("no package metadata for file %s", uri) - } - return mps[0], nil + return snapshot.NarrowestMetadataForFile(ctx, uri) } // NarrowestPackageForFile is a convenience function that selects the narrowest diff --git a/gopls/internal/server/definition.go b/gopls/internal/server/definition.go index 7b4df3c7c07..5a9c020cfc5 100644 --- a/gopls/internal/server/definition.go +++ b/gopls/internal/server/definition.go @@ -9,6 +9,7 @@ import ( "fmt" "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/goasm" "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" @@ -37,6 +38,8 @@ func (s *server) Definition(ctx context.Context, params *protocol.DefinitionPara return template.Definition(snapshot, fh, params.Position) case file.Go: return golang.Definition(ctx, snapshot, fh, params.Position) + case file.Asm: + return goasm.Definition(ctx, snapshot, fh, params.Position) default: return nil, fmt.Errorf("can't find definitions for file type %s", kind) } diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index adc9df6c17d..170a9823cad 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go @@ -113,11 +113,12 @@ type EditorConfig struct { // Map of language ID -> regexp to match, used to set the file type of new // buffers. Applied as an overlay on top of the following defaults: - // "go" -> ".*\.go" + // "go" -> ".*\.go" // "go.mod" -> "go\.mod" // "go.sum" -> "go\.sum" // "gotmpl" -> ".*tmpl" - FileAssociations map[string]string + // "go.s" -> ".*\.s" + FileAssociations map[protocol.LanguageKind]string // Settings holds user-provided configuration for the LSP server. Settings map[string]any @@ -619,27 +620,28 @@ func (e *Editor) sendDidOpen(ctx context.Context, item protocol.TextDocumentItem return nil } -var defaultFileAssociations = map[string]*regexp.Regexp{ +var defaultFileAssociations = map[protocol.LanguageKind]*regexp.Regexp{ "go": regexp.MustCompile(`^.*\.go$`), // '$' is important: don't match .gotmpl! "go.mod": regexp.MustCompile(`^go\.mod$`), "go.sum": regexp.MustCompile(`^go(\.work)?\.sum$`), "go.work": regexp.MustCompile(`^go\.work$`), "gotmpl": regexp.MustCompile(`^.*tmpl$`), + "go.s": regexp.MustCompile(`\.s$`), } // languageID returns the language identifier for the path p given the user // configured fileAssociations. -func languageID(p string, fileAssociations map[string]string) protocol.LanguageKind { +func languageID(p string, fileAssociations map[protocol.LanguageKind]string) protocol.LanguageKind { base := path.Base(p) for lang, re := range fileAssociations { re := regexp.MustCompile(re) if re.MatchString(base) { - return protocol.LanguageKind(lang) + return lang } } for lang, re := range defaultFileAssociations { if re.MatchString(base) { - return protocol.LanguageKind(lang) + return lang } } return "" diff --git a/gopls/internal/test/marker/testdata/definition/asm.txt b/gopls/internal/test/marker/testdata/definition/asm.txt new file mode 100644 index 00000000000..f0187d7e24a --- /dev/null +++ b/gopls/internal/test/marker/testdata/definition/asm.txt @@ -0,0 +1,33 @@ +This test exercises the Definition request in a Go assembly file. + +For now we support only references to package-level symbols defined in +the same package or a dependency. + +Repeatedly jumping to Definition on ff ping-pongs between the Go and +assembly declarations. + +-- go.mod -- +module example.com +go 1.18 + +-- a/a.go -- +package a + +import _ "fmt" +import _ "example.com/b" + +func ff() //@ loc(ffgo, re"()ff"), def("ff", ffasm) + +var _ = ff // pacify unusedfunc analyzer + +-- a/asm.s -- +// portable assembly + +TEXT ·ff(SB), $16 //@ loc(ffasm, "ff"), def("ff", ffgo) + CALL example·com∕b·B //@ def("com", bB) + JMP ·ff //@ def("ff", ffgo) + +-- b/b.go -- +package b + +func B() {} //@ loc(bB, re"()B") diff --git a/internal/expect/extract.go b/internal/expect/extract.go index 150a2afbbf6..8ad1cb259e5 100644 --- a/internal/expect/extract.go +++ b/internal/expect/extract.go @@ -8,7 +8,9 @@ import ( "fmt" "go/ast" "go/parser" + goscanner "go/scanner" "go/token" + "os" "path/filepath" "regexp" "strconv" @@ -32,21 +34,54 @@ type Identifier string // See the package documentation for details about the syntax of those // notes. func Parse(fset *token.FileSet, filename string, content []byte) ([]*Note, error) { - var src any - if content != nil { - src = content + if content == nil { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + content = data } + switch filepath.Ext(filename) { + case ".s": + // The assembler uses a custom scanner, + // but the go/scanner package is close + // enough: we only want the comments. + file := fset.AddFile(filename, -1, len(content)) + var scan goscanner.Scanner + scan.Init(file, content, nil, goscanner.ScanComments) + + var notes []*Note + for { + pos, tok, lit := scan.Scan() + if tok == token.EOF { + break + } + if tok == token.COMMENT { + text, adjust := getAdjustedNote(lit) + if text == "" { + continue + } + parsed, err := parse(fset, pos+token.Pos(adjust), text) + if err != nil { + return nil, err + } + notes = append(notes, parsed...) + } + } + return notes, nil + case ".go": - // TODO: We should write this in terms of the scanner. + // TODO: We should write this in terms of the scanner, like the .s case above. // there are ways you can break the parser such that it will not add all the // comments to the ast, which may result in files where the tests are silently // not run. - file, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.AllErrors|parser.SkipObjectResolution) + file, err := parser.ParseFile(fset, filename, content, parser.ParseComments|parser.AllErrors|parser.SkipObjectResolution) if file == nil { return nil, err } return ExtractGo(fset, file) + case ".mod": file, err := modfile.Parse(filename, content, nil) if err != nil { @@ -64,6 +99,7 @@ func Parse(fset *token.FileSet, filename string, content []byte) ([]*Note, error note.Pos += token.Pos(f.Base()) } return notes, nil + case ".work": file, err := modfile.ParseWork(filename, content, nil) if err != nil { From 9f7a2b618a10d26b9bc935167355490a7c32a20b Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 20 Feb 2025 16:01:31 -0500 Subject: [PATCH 47/99] gopls/doc/features: tweak markdown Previously the seems to cause underlining of what follows; see https://github.com/golang/tools/blob/master/gopls/doc/features/diagnostics.md. This fix is kind of a stab in the dark. Change-Id: Ic552faae8d03b3d49c1a913ef7e3a145add5cfc4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651096 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/doc/features/diagnostics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/doc/features/diagnostics.md b/gopls/doc/features/diagnostics.md index ceec607c123..6be7a43493a 100644 --- a/gopls/doc/features/diagnostics.md +++ b/gopls/doc/features/diagnostics.md @@ -51,7 +51,7 @@ build`. Gopls doesn't actually run the compiler; that would be too There is an optional third source of diagnostics: - + - **Compiler optimization details** are diagnostics that report details relevant to optimization decisions made by the Go From 33f1ed9242128736ca381ce86d10a5fc479aab4c Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Thu, 20 Feb 2025 10:41:48 -0800 Subject: [PATCH 48/99] gopls/go.mod: update dependencies following the v0.18.0 release This is an automated CL which updates the go.mod and go.sum. For golang/go#71607 Change-Id: Ic4d3e8174be60eca3f4799c0d3a99dd8f9017320 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651116 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Knyszek Auto-Submit: Gopher Robot --- gopls/go.mod | 12 ++++++------ gopls/go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/gopls/go.mod b/gopls/go.mod index 83620720ae6..f6a2b0a1e9a 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -10,20 +10,20 @@ require ( golang.org/x/mod v0.23.0 golang.org/x/sync v0.11.0 golang.org/x/sys v0.30.0 - golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9 + golang.org/x/telemetry v0.0.0-20250220152412-165e2f84edbc golang.org/x/text v0.22.0 - golang.org/x/tools v0.28.0 - golang.org/x/vuln v1.1.3 + golang.org/x/tools v0.30.0 + golang.org/x/vuln v1.1.4 gopkg.in/yaml.v3 v3.0.1 - honnef.co/go/tools v0.5.1 + honnef.co/go/tools v0.6.0 mvdan.cc/gofumpt v0.7.0 - mvdan.cc/xurls/v2 v2.5.0 + mvdan.cc/xurls/v2 v2.6.0 ) require ( github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/google/safehtml v0.1.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884 // indirect + golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/gopls/go.sum b/gopls/go.sum index b2b3d925a78..ef93b2c4601 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -12,13 +12,13 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM= +github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884 h1:1xaZTydL5Gsg78QharTwKfA9FY9CZ1VQj6D/AZEvHR0= -golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa h1:Br3+0EZZohShrmVVc85znGpxw7Ca8hsUJlrdT/JQGw8= +golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= @@ -36,8 +36,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= -golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9 h1:L2k9GUV2TpQKVRGMjN94qfUMgUwOFimSQ6gipyJIjKw= -golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9/go.mod h1:8h4Hgq+jcTvCDv2+i7NrfWwpYHcESleo2nGHxLbFLJ4= +golang.org/x/telemetry v0.0.0-20250220152412-165e2f84edbc h1:HS+G1Mhh2dxM8ObutfYKdjfD7zpkyeP/UxeRnJpIZtQ= +golang.org/x/telemetry v0.0.0-20250220152412-165e2f84edbc/go.mod h1:bDzXkYUaHzz51CtDy5kh/jR4lgPxsdbqC37kp/dzhCc= 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.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= @@ -46,16 +46,16 @@ 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= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/vuln v1.1.3 h1:NPGnvPOTgnjBc9HTaUx+nj+EaUYxl5SJOWqaDYGaFYw= -golang.org/x/vuln v1.1.3/go.mod h1:7Le6Fadm5FOqE9C926BCD0g12NWyhg7cxV4BwcPFuNY= +golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= +golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= -honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= +honnef.co/go/tools v0.6.0 h1:TAODvD3knlq75WCp2nyGJtT4LeRV/o7NN9nYPeVJXf8= +honnef.co/go/tools v0.6.0/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= -mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= -mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= +mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= +mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= From 1f6c6d67720feea9eeba7e1eb23841e63f3ccc81 Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Thu, 20 Feb 2025 23:31:50 +0100 Subject: [PATCH 49/99] gopls/doc: adjust nvim-lspconfig link target The file was renamed in the github.com/neovim/nvim-lspconfig repository. Change-Id: I89a8dcbbb31c24d77f0ca00934df1916b338d460 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651195 Reviewed-by: Robert Findley Auto-Submit: Robert Findley Auto-Submit: Tobias Klauser Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/doc/vim.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/doc/vim.md b/gopls/doc/vim.md index e71482115ea..444a7d6ff31 100644 --- a/gopls/doc/vim.md +++ b/gopls/doc/vim.md @@ -230,5 +230,5 @@ require('lspconfig').gopls.setup({ [govim-install]: https://github.com/myitcv/govim/blob/master/README.md#govim---go-development-plugin-for-vim8 [nvim-docs]: https://neovim.io/doc/user/lsp.html [nvim-install]: https://github.com/neovim/neovim/wiki/Installing-Neovim -[nvim-lspconfig]: https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#gopls +[nvim-lspconfig]: https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#gopls [nvim-lspconfig-imports]: https://github.com/neovim/nvim-lspconfig/issues/115 From 96bfb60194183d530de41f887c48081f8c104a86 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 21 Feb 2025 09:36:24 -0500 Subject: [PATCH 50/99] gopls/internal/analysis/modernize: fix minmax bug The matcher for pattern 2 forgot to check that the IfStmt.Else subtree was nil, leading to unsound fixes. Updates golang/go#71847 Change-Id: I0919076c1af38012cedf3072ef5d1117e96a64b9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651375 Reviewed-by: Jonathan Amsterdam Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/modernize/minmax.go | 2 +- .../analysis/modernize/testdata/src/minmax/minmax.go | 12 ++++++++++++ .../modernize/testdata/src/minmax/minmax.go.golden | 12 ++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/gopls/internal/analysis/modernize/minmax.go b/gopls/internal/analysis/modernize/minmax.go index 1466e767fc7..8888383afec 100644 --- a/gopls/internal/analysis/modernize/minmax.go +++ b/gopls/internal/analysis/modernize/minmax.go @@ -95,7 +95,7 @@ func minmax(pass *analysis.Pass) { }) } - } else if prev, ok := curIfStmt.PrevSibling(); ok && isSimpleAssign(prev.Node()) { + } else if prev, ok := curIfStmt.PrevSibling(); ok && isSimpleAssign(prev.Node()) && ifStmt.Else == nil { fassign := prev.Node().(*ast.AssignStmt) // Have: lhs0 = rhs0; if a < b { lhs = rhs } diff --git a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go index 8fdc3bc2106..44ba7c9193a 100644 --- a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go +++ b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go @@ -103,3 +103,15 @@ func nopeNotAMinimum(x, y int) int { } return y } + +// Regression test for https://github.com/golang/go/issues/71847#issuecomment-2673491596 +func nopeHasElseBlock(x int) int { + y := x + // Before, this was erroneously reduced to y = max(x, 0) + if y < 0 { + y = 0 + } else { + y += 2 + } + return y +} diff --git a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden index 48e154729e7..df1d5180f8a 100644 --- a/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden @@ -80,3 +80,15 @@ func nopeNotAMinimum(x, y int) int { } return y } + +// Regression test for https://github.com/golang/go/issues/71847#issuecomment-2673491596 +func nopeHasElseBlock(x int) int { + y := x + // Before, this was erroneously reduced to y = max(x, 0) + if y < 0 { + y = 0 + } else { + y += 2 + } + return y +} From f95771e6301730bd96b5ece4dfb6df630c070e83 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 21 Feb 2025 09:43:42 -0500 Subject: [PATCH 51/99] gopls/go.mod: update to go1.24 No code changes yet. Change-Id: Ibdf2dfab2bf282aea4f1bb7d0787fb60d81ebbdb Reviewed-on: https://go-review.googlesource.com/c/tools/+/651395 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- gopls/go.mod | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gopls/go.mod b/gopls/go.mod index f6a2b0a1e9a..210943206b8 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -1,8 +1,6 @@ module golang.org/x/tools/gopls -// go 1.23.1 fixes some bugs in go/types Alias support (golang/go#68894, golang/go#68905). -// go 1.23.4 fixes a miscompilation of range-over-func (golang/go#70035). -go 1.23.4 +go 1.24.0 require ( github.com/google/go-cmp v0.6.0 From 8b85edcc2f1f820a72c251a20869722780356f0a Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 21 Feb 2025 10:05:18 -0500 Subject: [PATCH 52/99] gopls/internal: use go1.24-isms This CL intentionally does not include any new API covered by the modernizers, whose fixes will be submitted separately. Surprisingly few changes in all. Change-Id: I0c45ed674fd80234e7c76823a23b8b3af3011835 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651376 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/gofix/directive.go | 2 +- gopls/internal/golang/pkgdoc.go | 17 +---------------- .../test/integration/misc/references_test.go | 2 +- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/gopls/internal/analysis/gofix/directive.go b/gopls/internal/analysis/gofix/directive.go index 796feb5189e..20c45313cfb 100644 --- a/gopls/internal/analysis/gofix/directive.go +++ b/gopls/internal/analysis/gofix/directive.go @@ -12,7 +12,7 @@ import ( // -- plundered from the future (CL 605517, issue #68021) -- -// TODO(adonovan): replace with ast.Directive after go1.24 (#68021). +// TODO(adonovan): replace with ast.Directive after go1.25 (#68021). // Beware of our local mods to handle analysistest // "want" comments on the same line. diff --git a/gopls/internal/golang/pkgdoc.go b/gopls/internal/golang/pkgdoc.go index a5f9cc97fa4..2faff1a1526 100644 --- a/gopls/internal/golang/pkgdoc.go +++ b/gopls/internal/golang/pkgdoc.go @@ -39,7 +39,6 @@ import ( "go/token" "go/types" "html" - "iter" "path/filepath" "slices" "strings" @@ -666,7 +665,7 @@ window.addEventListener('load', function() { cloneTparams(sig.RecvTypeParams()), cloneTparams(sig.TypeParams()), types.NewTuple(append( - slices.Collect(tupleVariables(sig.Params()))[:3], + slices.Collect(sig.Params().Variables())[:3], types.NewParam(0, nil, "", types.Typ[types.Invalid]))...), sig.Results(), false) // any final ...T parameter is truncated @@ -851,17 +850,3 @@ window.addEventListener('load', function() { return buf.Bytes(), nil } - -// tupleVariables returns a go1.23 iterator over the variables of a tuple type. -// -// Example: for v := range tuple.Variables() { ... } -// TODO(adonovan): use t.Variables in go1.24. -func tupleVariables(t *types.Tuple) iter.Seq[*types.Var] { - return func(yield func(v *types.Var) bool) { - for i := range t.Len() { - if !yield(t.At(i)) { - break - } - } - } -} diff --git a/gopls/internal/test/integration/misc/references_test.go b/gopls/internal/test/integration/misc/references_test.go index e84dcd71dc3..58fdb3c5cd8 100644 --- a/gopls/internal/test/integration/misc/references_test.go +++ b/gopls/internal/test/integration/misc/references_test.go @@ -126,7 +126,7 @@ var _ = unsafe.Slice(nil, 0) Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a.go") - for _, name := range strings.Fields( + for name := range strings.FieldsSeq( "iota error int nil append iota Pointer Sizeof Alignof Add Slice") { loc := env.RegexpSearch("a.go", `\b`+name+`\b`) From 23211ff47d7fe7c3bf662f2a3bf33d9c0ba57f31 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 20 Feb 2025 22:18:10 +0000 Subject: [PATCH 53/99] gopls/internal/test/integration: better expectation failures A particularly tricky-to-diagnose marker test failure finally led me to address several long-standing TODOs: integration test expectations should identify their specific failure reason. Previously, we had been relying on a combination the State.String summary and LSP logs to debug failed expectations, but often it was not obvious from the test failure what condition actually failed. Now, expectations describe their failure, and composite expectations compose their component failures. Change-Id: I2533c8a35b4eb561f505fd3ed95fe55483340773 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651417 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley --- gopls/internal/test/integration/env.go | 107 ++---- .../internal/test/integration/expectation.go | 307 ++++++++++-------- 2 files changed, 194 insertions(+), 220 deletions(-) diff --git a/gopls/internal/test/integration/env.go b/gopls/internal/test/integration/env.go index c8a1b5043aa..f19a426316d 100644 --- a/gopls/internal/test/integration/env.go +++ b/gopls/internal/test/integration/env.go @@ -114,53 +114,16 @@ type workProgress struct { complete bool // seen 'end' } -// This method, provided for debugging, accesses mutable fields without a lock, -// so it must not be called concurrent with any State mutation. -func (s State) String() string { - var b strings.Builder - b.WriteString("#### log messages (see RPC logs for full text):\n") - for _, msg := range s.logs { - summary := fmt.Sprintf("%v: %q", msg.Type, msg.Message) - if len(summary) > 60 { - summary = summary[:57] + "..." - } - // Some logs are quite long, and since they should be reproduced in the RPC - // logs on any failure we include here just a short summary. - fmt.Fprint(&b, "\t"+summary+"\n") - } - b.WriteString("\n") - b.WriteString("#### diagnostics:\n") - for name, params := range s.diagnostics { - fmt.Fprintf(&b, "\t%s (version %d):\n", name, params.Version) - for _, d := range params.Diagnostics { - fmt.Fprintf(&b, "\t\t%d:%d [%s]: %s\n", d.Range.Start.Line, d.Range.Start.Character, d.Source, d.Message) - } - } - b.WriteString("\n") - b.WriteString("#### outstanding work:\n") - for token, state := range s.work { - if state.complete { - continue - } - name := state.title - if name == "" { - name = fmt.Sprintf("!NO NAME(token: %s)", token) - } - fmt.Fprintf(&b, "\t%s: %.2f\n", name, state.percent) - } - b.WriteString("#### completed work:\n") - for name, count := range s.completedWork { - fmt.Fprintf(&b, "\t%s: %d\n", name, count) - } - return b.String() +type awaitResult struct { + verdict Verdict + reason string } -// A condition is satisfied when all expectations are simultaneously -// met. At that point, the 'met' channel is closed. On any failure, err is set -// and the failed channel is closed. +// A condition is satisfied when its expectation is [Met] or [Unmeetable]. The +// result is sent on the verdict channel. type condition struct { - expectations []Expectation - verdict chan Verdict + expectation Expectation + verdict chan awaitResult } func (a *Awaiter) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error { @@ -334,27 +297,13 @@ func (a *Awaiter) onUnregisterCapability(_ context.Context, m *protocol.Unregist func (a *Awaiter) checkConditionsLocked() { for id, condition := range a.waiters { - if v, _ := checkExpectations(a.state, condition.expectations); v != Unmet { + if v, why := condition.expectation.Check(a.state); v != Unmet { delete(a.waiters, id) - condition.verdict <- v + condition.verdict <- awaitResult{v, why} } } } -// checkExpectations reports whether s meets all expectations. -func checkExpectations(s State, expectations []Expectation) (Verdict, string) { - finalVerdict := Met - var summary strings.Builder - for _, e := range expectations { - v := e.Check(s) - if v > finalVerdict { - finalVerdict = v - } - fmt.Fprintf(&summary, "%v: %s\n", v, e.Description) - } - return finalVerdict, summary.String() -} - // Await blocks until the given expectations are all simultaneously met. // // Generally speaking Await should be avoided because it blocks indefinitely if @@ -363,7 +312,7 @@ func checkExpectations(s State, expectations []Expectation) (Verdict, string) { // waiting. func (e *Env) Await(expectations ...Expectation) { e.T.Helper() - if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil { + if err := e.Awaiter.Await(e.Ctx, AllOf(expectations...)); err != nil { e.T.Fatal(err) } } @@ -371,30 +320,30 @@ func (e *Env) Await(expectations ...Expectation) { // OnceMet blocks until the precondition is met by the state or becomes // unmeetable. If it was met, OnceMet checks that the state meets all // expectations in mustMeets. -func (e *Env) OnceMet(precondition Expectation, mustMeets ...Expectation) { +func (e *Env) OnceMet(pre Expectation, mustMeets ...Expectation) { e.T.Helper() - e.Await(OnceMet(precondition, mustMeets...)) + e.Await(OnceMet(pre, AllOf(mustMeets...))) } // Await waits for all expectations to simultaneously be met. It should only be // called from the main test goroutine. -func (a *Awaiter) Await(ctx context.Context, expectations ...Expectation) error { +func (a *Awaiter) Await(ctx context.Context, expectation Expectation) error { a.mu.Lock() // Before adding the waiter, we check if the condition is currently met or // failed to avoid a race where the condition was realized before Await was // called. - switch verdict, summary := checkExpectations(a.state, expectations); verdict { + switch verdict, why := expectation.Check(a.state); verdict { case Met: a.mu.Unlock() return nil case Unmeetable: - err := fmt.Errorf("unmeetable expectations:\n%s\nstate:\n%v", summary, a.state) + err := fmt.Errorf("unmeetable expectation:\n%s\nreason:\n%s", indent(expectation.Description), indent(why)) a.mu.Unlock() return err } cond := &condition{ - expectations: expectations, - verdict: make(chan Verdict), + expectation: expectation, + verdict: make(chan awaitResult), } a.waiters[nextAwaiterRegistration.Add(1)] = cond a.mu.Unlock() @@ -403,19 +352,17 @@ func (a *Awaiter) Await(ctx context.Context, expectations ...Expectation) error select { case <-ctx.Done(): err = ctx.Err() - case v := <-cond.verdict: - if v != Met { - err = fmt.Errorf("condition has final verdict %v", v) + case res := <-cond.verdict: + if res.verdict != Met { + err = fmt.Errorf("the following condition is %s:\n%s\nreason:\n%s", + res.verdict, indent(expectation.Description), indent(res.reason)) } } - a.mu.Lock() - defer a.mu.Unlock() - _, summary := checkExpectations(a.state, expectations) + return err +} - // Debugging an unmet expectation can be tricky, so we put some effort into - // nicely formatting the failure. - if err != nil { - return fmt.Errorf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, a.state) - } - return nil +// indent indents all lines of msg, including the first. +func indent(msg string) string { + const prefix = " " + return prefix + strings.ReplaceAll(msg, "\n", "\n"+prefix) } diff --git a/gopls/internal/test/integration/expectation.go b/gopls/internal/test/integration/expectation.go index fdfca90796e..70a16fd6b3a 100644 --- a/gopls/internal/test/integration/expectation.go +++ b/gopls/internal/test/integration/expectation.go @@ -5,14 +5,17 @@ package integration import ( + "bytes" "fmt" + "maps" "regexp" - "sort" + "slices" "strings" "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/server" + "golang.org/x/tools/gopls/internal/util/constraints" ) var ( @@ -55,16 +58,11 @@ func (v Verdict) String() string { // // Expectations are combinators. By composing them, tests may express // complex expectations in terms of simpler ones. -// -// TODO(rfindley): as expectations are combined, it becomes harder to identify -// why they failed. A better signature for Check would be -// -// func(State) (Verdict, string) -// -// returning a reason for the verdict that can be composed similarly to -// descriptions. type Expectation struct { - Check func(State) Verdict + // Check returns the verdict of this expectation for the given state. + // If the vertict is not [Met], the second result should return a reason + // that the verdict is not (yet) met. + Check func(State) (Verdict, string) // Description holds a noun-phrase identifying what the expectation checks. // @@ -74,117 +72,117 @@ type Expectation struct { // OnceMet returns an Expectation that, once the precondition is met, asserts // that mustMeet is met. -func OnceMet(precondition Expectation, mustMeets ...Expectation) Expectation { - check := func(s State) Verdict { - switch pre := precondition.Check(s); pre { - case Unmeetable: - return Unmeetable +func OnceMet(pre, post Expectation) Expectation { + check := func(s State) (Verdict, string) { + switch v, why := pre.Check(s); v { + case Unmeetable, Unmet: + return v, fmt.Sprintf("precondition is %s: %s", v, why) case Met: - for _, mustMeet := range mustMeets { - verdict := mustMeet.Check(s) - if verdict != Met { - return Unmeetable - } + v, why := post.Check(s) + if v != Met { + return Unmeetable, fmt.Sprintf("postcondition is not met:\n%s", indent(why)) } - return Met + return Met, "" default: - return Unmet + panic(fmt.Sprintf("unknown precondition verdict %s", v)) } } - description := describeExpectations(mustMeets...) + desc := fmt.Sprintf("once the following is met:\n%s\nmust have:\n%s", + indent(pre.Description), indent(post.Description)) return Expectation{ Check: check, - Description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description, description), - } -} - -func describeExpectations(expectations ...Expectation) string { - var descriptions []string - for _, e := range expectations { - descriptions = append(descriptions, e.Description) + Description: desc, } - return strings.Join(descriptions, "\n") } // Not inverts the sense of an expectation: a met expectation is unmet, and an // unmet expectation is met. func Not(e Expectation) Expectation { - check := func(s State) Verdict { - switch v := e.Check(s); v { + check := func(s State) (Verdict, string) { + switch v, _ := e.Check(s); v { case Met: - return Unmet + return Unmet, "condition unexpectedly satisfied" case Unmet, Unmeetable: - return Met + return Met, "" default: panic(fmt.Sprintf("unexpected verdict %v", v)) } } - description := describeExpectations(e) return Expectation{ Check: check, - Description: fmt.Sprintf("not: %s", description), + Description: fmt.Sprintf("not: %s", e.Description), } } // AnyOf returns an expectation that is satisfied when any of the given // expectations is met. func AnyOf(anyOf ...Expectation) Expectation { - check := func(s State) Verdict { + if len(anyOf) == 1 { + return anyOf[0] // avoid unnecessary boilerplate + } + check := func(s State) (Verdict, string) { for _, e := range anyOf { - verdict := e.Check(s) + verdict, _ := e.Check(s) if verdict == Met { - return Met + return Met, "" } } - return Unmet + return Unmet, "none of the expectations were met" } description := describeExpectations(anyOf...) return Expectation{ Check: check, - Description: fmt.Sprintf("Any of:\n%s", description), + Description: fmt.Sprintf("any of:\n%s", description), } } // AllOf expects that all given expectations are met. -// -// TODO(rfindley): the problem with these types of combinators (OnceMet, AnyOf -// and AllOf) is that we lose the information of *why* they failed: the Awaiter -// is not smart enough to look inside. -// -// Refactor the API such that the Check function is responsible for explaining -// why an expectation failed. This should allow us to significantly improve -// test output: we won't need to summarize state at all, as the verdict -// explanation itself should describe clearly why the expectation not met. func AllOf(allOf ...Expectation) Expectation { - check := func(s State) Verdict { - verdict := Met + if len(allOf) == 1 { + return allOf[0] // avoid unnecessary boilerplate + } + check := func(s State) (Verdict, string) { + var ( + verdict = Met + reason string + ) for _, e := range allOf { - if v := e.Check(s); v > verdict { + v, why := e.Check(s) + if v > verdict { verdict = v + reason = why } } - return verdict + return verdict, reason } - description := describeExpectations(allOf...) + desc := describeExpectations(allOf...) return Expectation{ Check: check, - Description: fmt.Sprintf("All of:\n%s", description), + Description: fmt.Sprintf("all of:\n%s", indent(desc)), } } +func describeExpectations(expectations ...Expectation) string { + var descriptions []string + for _, e := range expectations { + descriptions = append(descriptions, e.Description) + } + return strings.Join(descriptions, "\n") +} + // ReadDiagnostics is an Expectation that stores the current diagnostics for // fileName in into, whenever it is evaluated. // // It can be used in combination with OnceMet or AfterChange to capture the // state of diagnostics when other expectations are satisfied. func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { diags, ok := s.diagnostics[fileName] if !ok { - return Unmeetable + return Unmeetable, fmt.Sprintf("no diagnostics for %q", fileName) } *into = *diags - return Met + return Met, "" } return Expectation{ Check: check, @@ -198,13 +196,10 @@ func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) E // It can be used in combination with OnceMet or AfterChange to capture the // state of diagnostics when other expectations are satisfied. func ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Expectation { - check := func(s State) Verdict { - allDiags := make(map[string]*protocol.PublishDiagnosticsParams) - for name, diags := range s.diagnostics { - allDiags[name] = diags - } + check := func(s State) (Verdict, string) { + allDiags := maps.Clone(s.diagnostics) *into = allDiags - return Met + return Met, "" } return Expectation{ Check: check, @@ -215,13 +210,13 @@ func ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Exp // ShownDocument asserts that the client has received a // ShowDocumentRequest for the given URI. func ShownDocument(uri protocol.URI) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { for _, params := range s.showDocument { if params.URI == uri { - return Met + return Met, "" } } - return Unmet + return Unmet, fmt.Sprintf("no ShowDocumentRequest received for %s", uri) } return Expectation{ Check: check, @@ -236,9 +231,9 @@ func ShownDocument(uri protocol.URI) Expectation { // capture the set of showDocument requests when other expectations // are satisfied. func ShownDocuments(into *[]*protocol.ShowDocumentParams) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { *into = append(*into, s.showDocument...) - return Met + return Met, "" } return Expectation{ Check: check, @@ -247,31 +242,39 @@ func ShownDocuments(into *[]*protocol.ShowDocumentParams) Expectation { } // NoShownMessage asserts that the editor has not received a ShowMessage. -func NoShownMessage(subString string) Expectation { - check := func(s State) Verdict { +func NoShownMessage(containing string) Expectation { + check := func(s State) (Verdict, string) { for _, m := range s.showMessage { - if strings.Contains(m.Message, subString) { - return Unmeetable + if strings.Contains(m.Message, containing) { + // Format the message (which may contain newlines) as a block quote. + msg := fmt.Sprintf("\"\"\"\n%s\n\"\"\"", strings.TrimSpace(m.Message)) + return Unmeetable, fmt.Sprintf("observed the following message:\n%s", indent(msg)) } } - return Met + return Met, "" + } + var desc string + if containing != "" { + desc = fmt.Sprintf("received no ShowMessage containing %q", containing) + } else { + desc = "received no ShowMessage requests" } return Expectation{ Check: check, - Description: fmt.Sprintf("no ShowMessage received containing %q", subString), + Description: desc, } } // ShownMessage asserts that the editor has received a ShowMessageRequest // containing the given substring. func ShownMessage(containing string) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { for _, m := range s.showMessage { if strings.Contains(m.Message, containing) { - return Met + return Met, "" } } - return Unmet + return Unmet, fmt.Sprintf("no ShowMessage containing %q", containing) } return Expectation{ Check: check, @@ -281,22 +284,22 @@ func ShownMessage(containing string) Expectation { // ShownMessageRequest asserts that the editor has received a // ShowMessageRequest with message matching the given regular expression. -func ShownMessageRequest(messageRegexp string) Expectation { - msgRE := regexp.MustCompile(messageRegexp) - check := func(s State) Verdict { +func ShownMessageRequest(matchingRegexp string) Expectation { + msgRE := regexp.MustCompile(matchingRegexp) + check := func(s State) (Verdict, string) { if len(s.showMessageRequest) == 0 { - return Unmet + return Unmet, "no ShowMessageRequest have been received" } for _, m := range s.showMessageRequest { if msgRE.MatchString(m.Message) { - return Met + return Met, "" } } - return Unmet + return Unmet, fmt.Sprintf("no ShowMessageRequest (out of %d) match %q", len(s.showMessageRequest), matchingRegexp) } return Expectation{ Check: check, - Description: fmt.Sprintf("ShowMessageRequest matching %q", messageRegexp), + Description: fmt.Sprintf("ShowMessageRequest matching %q", matchingRegexp), } } @@ -328,9 +331,7 @@ func (e *Env) DoneDiagnosingChanges() Expectation { } // Sort for stability. - sort.Slice(expected, func(i, j int) bool { - return expected[i] < expected[j] - }) + slices.Sort(expected) var all []Expectation for _, source := range expected { @@ -411,15 +412,16 @@ func (e *Env) DoneWithClose() Expectation { // // See CompletedWork. func StartedWork(title string, atLeast uint64) Expectation { - check := func(s State) Verdict { - if s.startedWork[title] >= atLeast { - return Met + check := func(s State) (Verdict, string) { + started := s.startedWork[title] + if started >= atLeast { + return Met, "" } - return Unmet + return Unmet, fmt.Sprintf("started work %d %s", started, pluralize("time", started)) } return Expectation{ Check: check, - Description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast), + Description: fmt.Sprintf("started work %q at least %d %s", title, atLeast, pluralize("time", atLeast)), } } @@ -428,16 +430,16 @@ func StartedWork(title string, atLeast uint64) Expectation { // Since the Progress API doesn't include any hidden metadata, we must use the // progress notification title to identify the work we expect to be completed. func CompletedWork(title string, count uint64, atLeast bool) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { completed := s.completedWork[title] if completed == count || atLeast && completed > count { - return Met + return Met, "" } - return Unmet + return Unmet, fmt.Sprintf("completed %d %s", completed, pluralize("time", completed)) } - desc := fmt.Sprintf("completed work %q %v times", title, count) + desc := fmt.Sprintf("completed work %q %v %s", title, count, pluralize("time", count)) if atLeast { - desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count) + desc = fmt.Sprintf("completed work %q at least %d %s", title, count, pluralize("time", count)) } return Expectation{ Check: check, @@ -445,6 +447,14 @@ func CompletedWork(title string, count uint64, atLeast bool) Expectation { } } +// pluralize adds an 's' suffix to name if n > 1. +func pluralize[T constraints.Integer](name string, n T) string { + if n > 1 { + return name + "s" + } + return name +} + type WorkStatus struct { // Last seen message from either `begin` or `report` progress. Msg string @@ -459,24 +469,23 @@ type WorkStatus struct { // If the token is not a progress token that the client has seen, this // expectation is Unmeetable. func CompletedProgressToken(token protocol.ProgressToken, into *WorkStatus) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { work, ok := s.work[token] if !ok { - return Unmeetable // TODO(rfindley): refactor to allow the verdict to explain this result + return Unmeetable, "no matching work items" } if work.complete { if into != nil { into.Msg = work.msg into.EndMsg = work.endMsg } - return Met + return Met, "" } - return Unmet + return Unmet, fmt.Sprintf("work is not complete; last message: %q", work.msg) } - desc := fmt.Sprintf("completed work for token %v", token) return Expectation{ Check: check, - Description: desc, + Description: fmt.Sprintf("completed work for token %v", token), } } @@ -488,28 +497,27 @@ func CompletedProgressToken(token protocol.ProgressToken, into *WorkStatus) Expe // This expectation is a vestige of older workarounds for asynchronous command // execution. func CompletedProgress(title string, into *WorkStatus) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { var work *workProgress for _, w := range s.work { if w.title == title { if work != nil { - // TODO(rfindley): refactor to allow the verdict to explain this result - return Unmeetable // multiple matches + return Unmeetable, "multiple matching work items" } work = w } } if work == nil { - return Unmeetable // zero matches + return Unmeetable, "no matching work items" } if work.complete { if into != nil { into.Msg = work.msg into.EndMsg = work.endMsg } - return Met + return Met, "" } - return Unmet + return Unmet, fmt.Sprintf("work is not complete; last message: %q", work.msg) } desc := fmt.Sprintf("exactly 1 completed workDoneProgress with title %v", title) return Expectation{ @@ -522,16 +530,16 @@ func CompletedProgress(title string, into *WorkStatus) Expectation { // be an exact match, whereas the given msg must only be contained in the work // item's message. func OutstandingWork(title, msg string) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { for _, work := range s.work { if work.complete { continue } if work.title == title && strings.Contains(work.msg, msg) { - return Met + return Met, "" } } - return Unmet + return Unmet, "no matching work" } return Expectation{ Check: check, @@ -548,7 +556,7 @@ func OutstandingWork(title, msg string) Expectation { // TODO(rfindley): consider refactoring to treat outstanding work the same way // we treat diagnostics: with an algebra of filters. func NoOutstandingWork(ignore func(title, msg string) bool) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { for _, w := range s.work { if w.complete { continue @@ -563,9 +571,9 @@ func NoOutstandingWork(ignore func(title, msg string) bool) Expectation { if ignore != nil && ignore(w.title, w.msg) { continue } - return Unmet + return Unmet, fmt.Sprintf("found outstanding work %q: %q", w.title, w.msg) } - return Met + return Met, "" } return Expectation{ Check: check, @@ -600,7 +608,7 @@ func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) E if err != nil { panic(err) } - check := func(state State) Verdict { + check := func(state State) (Verdict, string) { var found int for _, msg := range state.logs { if msg.Type == typ && rec.Match([]byte(msg.Message)) { @@ -609,14 +617,15 @@ func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) E } // Check for an exact or "at least" match. if found == count || (found >= count && atLeast) { - return Met + return Met, "" } // If we require an exact count, and have received more than expected, the // expectation can never be met. + verdict := Unmet if found > count && !atLeast { - return Unmeetable + verdict = Unmeetable } - return Unmet + return verdict, fmt.Sprintf("found %d matching logs", found) } desc := fmt.Sprintf("log message matching %q expected %v times", re, count) if atLeast { @@ -640,20 +649,24 @@ func NoLogMatching(typ protocol.MessageType, re string) Expectation { panic(err) } } - check := func(state State) Verdict { + check := func(state State) (Verdict, string) { for _, msg := range state.logs { if msg.Type != typ { continue } if r == nil || r.Match([]byte(msg.Message)) { - return Unmeetable + return Unmeetable, fmt.Sprintf("found matching log %q", msg.Message) } } - return Met + return Met, "" + } + desc := fmt.Sprintf("no %s log messages", typ) + if re != "" { + desc += fmt.Sprintf(" matching %q", re) } return Expectation{ Check: check, - Description: fmt.Sprintf("no log message matching %q", re), + Description: desc, } } @@ -673,18 +686,18 @@ func NoFileWatchMatching(re string) Expectation { } } -func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict { +func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) (Verdict, string) { rec := regexp.MustCompile(re) - return func(s State) Verdict { + return func(s State) (Verdict, string) { r := s.registeredCapabilities["workspace/didChangeWatchedFiles"] watchers := jsonProperty(r.RegisterOptions, "watchers").([]any) for _, watcher := range watchers { pattern := jsonProperty(watcher, "globPattern").(string) if rec.MatchString(pattern) { - return onMatch + return onMatch, fmt.Sprintf("matches watcher pattern %q", pattern) } } - return onNoMatch + return onNoMatch, "no matching watchers" } } @@ -707,10 +720,14 @@ func jsonProperty(obj any, path ...string) any { return jsonProperty(m[path[0]], path[1:]...) } +func formatDiagnostic(d protocol.Diagnostic) string { + return fmt.Sprintf("%d:%d [%s]: %s\n", d.Range.Start.Line, d.Range.Start.Character, d.Source, d.Message) +} + // Diagnostics asserts that there is at least one diagnostic matching the given // filters. func Diagnostics(filters ...DiagnosticFilter) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { diags := flattenDiagnostics(s) for _, filter := range filters { var filtered []flatDiagnostic @@ -720,14 +737,22 @@ func Diagnostics(filters ...DiagnosticFilter) Expectation { } } if len(filtered) == 0 { - // TODO(rfindley): if/when expectations describe their own failure, we - // can provide more useful information here as to which filter caused - // the failure. - return Unmet + // Reprinting the description of the filters is too verbose. + // + // We can probably do better here, but for now just format the + // diagnostics. + var b bytes.Buffer + for name, params := range s.diagnostics { + fmt.Fprintf(&b, "\t%s (version %d):\n", name, params.Version) + for _, d := range params.Diagnostics { + fmt.Fprintf(&b, "\t\t%s", formatDiagnostic(d)) + } + } + return Unmet, fmt.Sprintf("diagnostics:\n%s", b.String()) } diags = filtered } - return Met + return Met, "" } var descs []string for _, filter := range filters { @@ -743,7 +768,7 @@ func Diagnostics(filters ...DiagnosticFilter) Expectation { // filters. Notably, if no filters are supplied this assertion checks that // there are no diagnostics at all, for any file. func NoDiagnostics(filters ...DiagnosticFilter) Expectation { - check := func(s State) Verdict { + check := func(s State) (Verdict, string) { diags := flattenDiagnostics(s) for _, filter := range filters { var filtered []flatDiagnostic @@ -755,9 +780,11 @@ func NoDiagnostics(filters ...DiagnosticFilter) Expectation { diags = filtered } if len(diags) > 0 { - return Unmet + d := diags[0] + why := fmt.Sprintf("have diagnostic: %s: %v", d.name, formatDiagnostic(d.diag)) + return Unmet, why } - return Met + return Met, "" } var descs []string for _, filter := range filters { From f2beb33b192b2c3cfca5cc80b88d1d46abc058a7 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 21 Feb 2025 17:47:43 +0000 Subject: [PATCH 54/99] gopls: temporarily reinstate the "Structured" hover kind As described in golang/go#71879, the removal of the experimental "Structured" hover kind unexpectedly broke vim-go. Reinstate support for this setting, with tests, so that we can proceed with its deprecation more cautiously. For golang/go#71879 Change-Id: I6d22852aa10126c84b66f4345fbbdcf4cefbd182 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651238 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Auto-Submit: Robert Findley --- gopls/doc/settings.md | 3 + gopls/internal/doc/api.json | 4 + gopls/internal/golang/hover.go | 136 ++++++++++-------- gopls/internal/settings/settings.go | 14 +- gopls/internal/settings/settings_test.go | 16 +-- .../test/marker/testdata/hover/json.txt | 33 +++++ 6 files changed, 135 insertions(+), 71 deletions(-) create mode 100644 gopls/internal/test/marker/testdata/hover/json.txt diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index d989b2d19b9..7aeab79a575 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -428,6 +428,9 @@ Must be one of: * `"FullDocumentation"` * `"NoDocumentation"` * `"SingleLine"` +* `"Structured"` is a misguided experimental setting that returns a JSON +hover format. This setting should not be used, as it will be removed in a +future release of gopls. * `"SynopsisDocumentation"` Default: `"FullDocumentation"`. diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 629e45ff766..b6e53d18558 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -134,6 +134,10 @@ "Value": "\"SingleLine\"", "Doc": "" }, + { + "Value": "\"Structured\"", + "Doc": "`\"Structured\"` is a misguided experimental setting that returns a JSON\nhover format. This setting should not be used, as it will be removed in a\nfuture release of gopls.\n" + }, { "Value": "\"SynopsisDocumentation\"", "Doc": "" diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 7fc584f2c1a..cda79dcadb8 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -7,6 +7,7 @@ package golang import ( "bytes" "context" + "encoding/json" "fmt" "go/ast" "go/constant" @@ -48,37 +49,47 @@ import ( // It is formatted in one of several formats as determined by the // HoverKind setting. type hoverResult struct { - // synopsis is a single sentence synopsis of the symbol's documentation. + // The fields below are exported to define the JSON hover format. + // TODO(golang/go#70233): (re)remove support for JSON hover. + + // Synopsis is a single sentence Synopsis of the symbol's documentation. // - // TODO(adonovan): in what syntax? It (usually) comes from doc.synopsis, + // TODO(adonovan): in what syntax? It (usually) comes from doc.Synopsis, // which produces "Text" form, but it may be fed to // DocCommentToMarkdown, which expects doc comment syntax. - synopsis string + Synopsis string - // fullDocumentation is the symbol's full documentation. - fullDocumentation string + // FullDocumentation is the symbol's full documentation. + FullDocumentation string - // signature is the symbol's signature. - signature string + // Signature is the symbol's Signature. + Signature string - // singleLine is a single line describing the symbol. + // SingleLine is a single line describing the symbol. // This is recommended only for use in clients that show a single line for hover. - singleLine string + SingleLine string - // symbolName is the human-readable name to use for the symbol in links. - symbolName string + // SymbolName is the human-readable name to use for the symbol in links. + SymbolName string - // linkPath is the path of the package enclosing the given symbol, + // LinkPath is the path of the package enclosing the given symbol, // with the module portion (if any) replaced by "module@version". // // For example: "github.com/google/go-github/v48@v48.1.0/github". // - // Use LinkTarget + "/" + linkPath + "#" + LinkAnchor to form a pkgsite URL. - linkPath string + // Use LinkTarget + "/" + LinkPath + "#" + LinkAnchor to form a pkgsite URL. + LinkPath string - // linkAnchor is the pkg.go.dev link anchor for the given symbol. + // LinkAnchor is the pkg.go.dev link anchor for the given symbol. // For example, the "Node" part of "pkg.go.dev/go/ast#Node". - linkAnchor string + LinkAnchor string + + // New fields go below, and are unexported. The existing + // exported fields are underspecified and have already + // constrained our movements too much. A detailed JSON + // interface might be nice, but it needs a design and a + // precise specification. + // TODO(golang/go#70233): (re)deprecate the JSON hover output. // typeDecl is the declaration syntax for a type, // or "" for a non-type. @@ -284,9 +295,9 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro typesinternal.SetVarKind(v, typesinternal.LocalVar) signature := types.ObjectString(v, qual) return *hoverRange, &hoverResult{ - signature: signature, - singleLine: signature, - symbolName: v.Name(), + Signature: signature, + SingleLine: signature, + SymbolName: v.Name(), }, nil } @@ -615,13 +626,13 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro } return *hoverRange, &hoverResult{ - synopsis: doc.Synopsis(docText), - fullDocumentation: docText, - singleLine: singleLineSignature, - symbolName: linkName, - signature: signature, - linkPath: linkPath, - linkAnchor: anchor, + Synopsis: doc.Synopsis(docText), + FullDocumentation: docText, + SingleLine: singleLineSignature, + SymbolName: linkName, + Signature: signature, + LinkPath: linkPath, + LinkAnchor: anchor, typeDecl: typeDecl, methods: methods, promotedFields: fields, @@ -638,8 +649,8 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec if obj.Name() == "Error" { signature := obj.String() return &hoverResult{ - signature: signature, - singleLine: signature, + Signature: signature, + SingleLine: signature, // TODO(rfindley): these are better than the current behavior. // SymbolName: "(error).Error", // LinkPath: "builtin", @@ -682,13 +693,13 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec docText := comment.Text() return &hoverResult{ - synopsis: doc.Synopsis(docText), - fullDocumentation: docText, - signature: signature, - singleLine: obj.String(), - symbolName: obj.Name(), - linkPath: "builtin", - linkAnchor: obj.Name(), + Synopsis: doc.Synopsis(docText), + FullDocumentation: docText, + Signature: signature, + SingleLine: obj.String(), + SymbolName: obj.Name(), + LinkPath: "builtin", + LinkAnchor: obj.Name(), }, nil } @@ -740,9 +751,9 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa docText := comment.Text() return rng, &hoverResult{ - signature: "package " + string(impMetadata.Name), - synopsis: doc.Synopsis(docText), - fullDocumentation: docText, + Signature: "package " + string(impMetadata.Name), + Synopsis: doc.Synopsis(docText), + FullDocumentation: docText, }, nil } @@ -798,9 +809,9 @@ func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *h } return rng, &hoverResult{ - signature: "package " + string(pkg.Metadata().Name), - synopsis: doc.Synopsis(docText), - fullDocumentation: docText, + Signature: "package " + string(pkg.Metadata().Name), + Synopsis: doc.Synopsis(docText), + FullDocumentation: docText, footer: footer, }, nil } @@ -926,8 +937,8 @@ func hoverLit(pgf *parsego.File, lit *ast.BasicLit, pos token.Pos) (protocol.Ran } hover := b.String() return rng, &hoverResult{ - synopsis: hover, - fullDocumentation: hover, + Synopsis: hover, + FullDocumentation: hover, }, nil } @@ -966,7 +977,7 @@ func hoverReturnStatement(pgf *parsego.File, path []ast.Node, ret *ast.ReturnStm } buf.WriteByte(')') return rng, &hoverResult{ - signature: buf.String(), + Signature: buf.String(), }, nil } @@ -1005,9 +1016,9 @@ func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Ra } res := &hoverResult{ - signature: fmt.Sprintf("Embedding %q", pattern), - synopsis: s.String(), - fullDocumentation: s.String(), + Signature: fmt.Sprintf("Embedding %q", pattern), + Synopsis: s.String(), + FullDocumentation: s.String(), } return rng, res, nil } @@ -1242,10 +1253,17 @@ func formatHover(h *hoverResult, options *settings.Options, pkgURL func(path Pac switch options.HoverKind { case settings.SingleLine: - return h.singleLine, nil + return h.SingleLine, nil case settings.NoDocumentation: - return maybeFenced(h.signature), nil + return maybeFenced(h.Signature), nil + + case settings.Structured: + b, err := json.Marshal(h) + if err != nil { + return "", err + } + return string(b), nil case settings.SynopsisDocumentation, settings.FullDocumentation: var sections [][]string // assembled below @@ -1256,20 +1274,20 @@ func formatHover(h *hoverResult, options *settings.Options, pkgURL func(path Pac // but not Signature, which is redundant (= TypeDecl + "\n" + Methods). // For all other symbols, we display Signature; // TypeDecl and Methods are empty. - // (Now that JSON is no more, we could rationalize this.) + // TODO(golang/go#70233): When JSON is no more, we could rationalize this. if h.typeDecl != "" { sections = append(sections, []string{maybeFenced(h.typeDecl)}) } else { - sections = append(sections, []string{maybeFenced(h.signature)}) + sections = append(sections, []string{maybeFenced(h.Signature)}) } // Doc section. var doc string switch options.HoverKind { case settings.SynopsisDocumentation: - doc = h.synopsis + doc = h.Synopsis case settings.FullDocumentation: - doc = h.fullDocumentation + doc = h.FullDocumentation } if options.PreferredContentFormat == protocol.Markdown { doc = DocCommentToMarkdown(doc, options) @@ -1392,7 +1410,7 @@ func StdSymbolOf(obj types.Object) *stdlib.Symbol { // If pkgURL is non-nil, it should be used to generate doc links. func formatLink(h *hoverResult, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) string { - if options.LinksInHover == settings.LinksInHover_None || h.linkPath == "" { + if options.LinksInHover == settings.LinksInHover_None || h.LinkPath == "" { return "" } var url protocol.URI @@ -1400,26 +1418,26 @@ func formatLink(h *hoverResult, options *settings.Options, pkgURL func(path Pack if pkgURL != nil { // LinksInHover == "gopls" // Discard optional module version portion. // (Ideally the hoverResult would retain the structure...) - path := h.linkPath - if module, versionDir, ok := strings.Cut(h.linkPath, "@"); ok { + path := h.LinkPath + if module, versionDir, ok := strings.Cut(h.LinkPath, "@"); ok { // "module@version/dir" path = module if _, dir, ok := strings.Cut(versionDir, "/"); ok { path += "/" + dir } } - url = pkgURL(PackagePath(path), h.linkAnchor) + url = pkgURL(PackagePath(path), h.LinkAnchor) caption = "in gopls doc viewer" } else { if options.LinkTarget == "" { return "" } - url = cache.BuildLink(options.LinkTarget, h.linkPath, h.linkAnchor) + url = cache.BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor) caption = "on " + options.LinkTarget } switch options.PreferredContentFormat { case protocol.Markdown: - return fmt.Sprintf("[`%s` %s](%s)", h.symbolName, caption, url) + return fmt.Sprintf("[`%s` %s](%s)", h.SymbolName, caption, url) case protocol.PlainText: return "" default: diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 393bccac312..7b04e6b746b 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -798,6 +798,11 @@ const ( NoDocumentation HoverKind = "NoDocumentation" SynopsisDocumentation HoverKind = "SynopsisDocumentation" FullDocumentation HoverKind = "FullDocumentation" + + // Structured is a misguided experimental setting that returns a JSON + // hover format. This setting should not be used, as it will be removed in a + // future release of gopls. + Structured HoverKind = "Structured" ) type VulncheckMode string @@ -1073,14 +1078,15 @@ func (o *Options) setOne(name string, value any) (applied []CounterPath, _ error AllSymbolScope) case "hoverKind": - if s, ok := value.(string); ok && strings.EqualFold(s, "structured") { - return nil, deprecatedError("the experimental hoverKind='structured' setting was removed in gopls/v0.18.0 (https://go.dev/issue/70233)") - } + // TODO(rfindley): reinstate the deprecation of Structured hover by making + // it a warning in gopls v0.N+1, and removing it in gopls v0.N+2. return setEnum(&o.HoverKind, value, NoDocumentation, SingleLine, SynopsisDocumentation, - FullDocumentation) + FullDocumentation, + Structured, + ) case "linkTarget": return nil, setString(&o.LinkTarget, value) diff --git a/gopls/internal/settings/settings_test.go b/gopls/internal/settings/settings_test.go index 05afa8ecac3..bd9ec110874 100644 --- a/gopls/internal/settings/settings_test.go +++ b/gopls/internal/settings/settings_test.go @@ -91,19 +91,19 @@ func TestOptions_Set(t *testing.T) { }, }, { - name: "hoverKind", - value: "Structured", - wantError: true, + name: "hoverKind", + value: "Structured", + // wantError: true, // TODO(rfindley): reinstate this error check: func(o Options) bool { - return o.HoverKind == FullDocumentation + return o.HoverKind == Structured }, }, { - name: "ui.documentation.hoverKind", - value: "Structured", - wantError: true, + name: "ui.documentation.hoverKind", + value: "Structured", + // wantError: true, // TODO(rfindley): reinstate this error check: func(o Options) bool { - return o.HoverKind == FullDocumentation + return o.HoverKind == Structured }, }, { diff --git a/gopls/internal/test/marker/testdata/hover/json.txt b/gopls/internal/test/marker/testdata/hover/json.txt new file mode 100644 index 00000000000..6c489cb4221 --- /dev/null +++ b/gopls/internal/test/marker/testdata/hover/json.txt @@ -0,0 +1,33 @@ +This test demonstrates support for "hoverKind": "Structured". + +Its size expectations assume a 64-bit machine. + +-- flags -- +-skip_goarch=386,arm + +-- go.mod -- +module example.com/p + +go 1.18 + +-- settings.json -- +{ + "hoverKind": "Structured" +} +-- p.go -- +package p + +// MyType is a type. +type MyType struct { //@ hover("MyType", "MyType", MyType) + F int // a field + S string // a string field +} + +// MyFunc is a function. +func MyFunc(i int) string { //@ hover("MyFunc", "MyFunc", MyFunc) + return "" +} +-- @MyFunc -- +{"Synopsis":"MyFunc is a function.","FullDocumentation":"MyFunc is a function.\n","Signature":"func MyFunc(i int) string","SingleLine":"func MyFunc(i int) string","SymbolName":"p.MyFunc","LinkPath":"example.com/p","LinkAnchor":"MyFunc"} +-- @MyType -- +{"Synopsis":"MyType is a type.","FullDocumentation":"MyType is a type.\n","Signature":"type MyType struct { // size=24 (0x18)\n\tF int // a field\n\tS string // a string field\n}\n","SingleLine":"type MyType struct{F int; S string}","SymbolName":"p.MyType","LinkPath":"example.com/p","LinkAnchor":"MyType"} From 7347766eee58ceaf6dc96b921cbd775f7844f267 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 20 Feb 2025 17:40:52 +0000 Subject: [PATCH 55/99] gopls/internal/test: fix failures when running tests with GOTOOLCHAIN Gopls integration tests want to use the ambient Go toolchain, to test integration with older Go commands. But GOTOOLCHAIN injects the toolchain binary into PATH, so gopls must remove this injected path element before it runs the go command. Unfortunately, if GOTOOLCHAIN=go1.N.P explicitly, those tests will also try to *download* the explicit toolchain and fail because we have set GOPROXY to a file based proxy. Fix this by first adding a check that the initial workspace load did not fail, as well as other related error annotations such that the failure message more accurately identifies the problem. Additionally, the preceding CL improved the integration test framework to better surface such errors. Then, actually fix the problem by setting GOTOOLCHAIN=local in our integration test sandbox. Change-Id: I8c7e9f10d1c17143f10be42476caf29021ab63e0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651418 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley --- .../internal/golang/completion/completion.go | 19 ++++++++++++++----- .../internal/test/integration/fake/sandbox.go | 1 + gopls/internal/test/marker/marker_test.go | 5 ++++- internal/imports/fix.go | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index 4c340055233..a6c0e49c311 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go @@ -668,7 +668,7 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p err = c.collectCompletions(ctx) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to collect completions: %v", err) } // Deep search collected candidates and their members for more candidates. @@ -688,7 +688,7 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p for _, callback := range c.completionCallbacks { if deadline == nil || time.Now().Before(*deadline) { if err := c.snapshot.RunProcessEnvFunc(ctx, callback); err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to run goimports callback: %v", err) } } } @@ -989,7 +989,10 @@ func (c *completer) populateImportCompletions(searchImport *ast.ImportSpec) erro } c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error { - return imports.GetImportPaths(ctx, searchImports, prefix, c.filename, c.pkg.Types().Name(), opts.Env) + if err := imports.GetImportPaths(ctx, searchImports, prefix, c.filename, c.pkg.Types().Name(), opts.Env); err != nil { + return fmt.Errorf("getting import paths: %v", err) + } + return nil }) return nil } @@ -1529,7 +1532,10 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error { c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error { defer cancel() - return imports.GetPackageExports(ctx, add, id.Name, c.filename, c.pkg.Types().Name(), opts.Env) + if err := imports.GetPackageExports(ctx, add, id.Name, c.filename, c.pkg.Types().Name(), opts.Env); err != nil { + return fmt.Errorf("getting package exports: %v", err) + } + return nil }) return nil } @@ -1916,7 +1922,10 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru } c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error { - return imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.Types().Name(), opts.Env) + if err := imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.Types().Name(), opts.Env); err != nil { + return fmt.Errorf("getting completion candidates: %v", err) + } + return nil }) return nil diff --git a/gopls/internal/test/integration/fake/sandbox.go b/gopls/internal/test/integration/fake/sandbox.go index 7adf3e3e4a9..1d8918babd4 100644 --- a/gopls/internal/test/integration/fake/sandbox.go +++ b/gopls/internal/test/integration/fake/sandbox.go @@ -208,6 +208,7 @@ func (sb *Sandbox) GoEnv() map[string]string { "GO111MODULE": "", "GOSUMDB": "off", "GOPACKAGESDRIVER": "off", + "GOTOOLCHAIN": "local", // tests should not download toolchains } if testenv.Go1Point() >= 5 { vars["GOMODCACHE"] = "" diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index 516dfeb3881..d7f91abed46 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -971,7 +971,10 @@ func newEnv(t *testing.T, cache *cache.Cache, files, proxyFiles map[string][]byt sandbox.Close() // ignore error t.Fatal(err) } - if err := awaiter.Await(ctx, integration.InitialWorkspaceLoad); err != nil { + if err := awaiter.Await(ctx, integration.OnceMet( + integration.InitialWorkspaceLoad, + integration.NoShownMessage(""), + )); err != nil { sandbox.Close() // ignore error t.Fatal(err) } diff --git a/internal/imports/fix.go b/internal/imports/fix.go index bf6b0aaddde..ee0efe48a55 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go @@ -1030,7 +1030,7 @@ func (e *ProcessEnv) GetResolver() (Resolver, error) { // // For gopls, we can optionally explicitly choose a resolver type, since we // already know the view type. - if len(e.Env["GOMOD"]) == 0 && len(e.Env["GOWORK"]) == 0 { + if e.Env["GOMOD"] == "" && (e.Env["GOWORK"] == "" || e.Env["GOWORK"] == "off") { e.resolver = newGopathResolver(e) e.logf("created gopath resolver") } else if r, err := newModuleResolver(e, e.ModCache); err != nil { From 4e0c888d60c4363071510deedaa07ca8cc9530ae Mon Sep 17 00:00:00 2001 From: xieyuschen Date: Tue, 21 Jan 2025 17:36:24 +0800 Subject: [PATCH 56/99] gopls/internal/hover: show alias rhs type declaration on hover This CL support to find the direct Rhs declaration for an alias type in hover. Fixes golang/go#71286 Change-Id: Ie43a70ec52fe41510e303bb538cc170ff59020c0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/644495 Auto-Submit: Robert Findley Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/golang/hover.go | 111 ++++++++++++------ .../test/marker/testdata/definition/embed.txt | 2 + .../marker/testdata/hover/hover_alias.txt | 81 +++++++++++++ 3 files changed, 156 insertions(+), 38 deletions(-) create mode 100644 gopls/internal/test/marker/testdata/hover/hover_alias.txt diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index cda79dcadb8..947595715a7 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -138,6 +138,28 @@ func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, positi }, nil } +// findRhsTypeDecl finds an alias's rhs type and returns its declaration. +// The rhs of an alias might be an alias as well, but we feel this is a rare case. +// It returns an empty string if the given obj is not an alias. +func findRhsTypeDecl(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, obj types.Object) (string, error) { + if alias, ok := obj.Type().(*types.Alias); ok { + // we choose Rhs instead of types.Unalias to make the connection between original alias + // and the corresponding aliased type clearer. + // types.Unalias brings confusion because it breaks the connection from A to C given + // the alias chain like 'type ( A = B; B =C ; )' except we show all transitive alias + // from start to the end. As it's rare, we don't do so. + t := alias.Rhs() + switch o := t.(type) { + case *types.Named: + obj = o.Obj() + declPGF1, declPos1, _ := parseFull(ctx, snapshot, pkg.FileSet(), obj.Pos()) + realTypeDecl, _, err := typeDeclContent(declPGF1, declPos1, obj) + return realTypeDecl, err + } + } + return "", nil +} + // hover computes hover information at the given position. If we do not support // hovering at the position, it returns _, nil, nil: an error is only returned // if the position is valid but we fail to compute hover information. @@ -404,46 +426,20 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro _, isTypeName := obj.(*types.TypeName) _, isTypeParam := types.Unalias(obj.Type()).(*types.TypeParam) if isTypeName && !isTypeParam { - spec, ok := spec.(*ast.TypeSpec) - if !ok { - // We cannot find a TypeSpec for this type or alias declaration - // (that is not a type parameter or a built-in). - // This should be impossible even for ill-formed trees; - // we suspect that AST repair may be creating inconsistent - // positions. Don't report a bug in that case. (#64241) - errorf := fmt.Errorf - if !declPGF.Fixed() { - errorf = bug.Errorf - } - return protocol.Range{}, nil, errorf("type name %q without type spec", obj.Name()) + var spec1 *ast.TypeSpec + typeDecl, spec1, err = typeDeclContent(declPGF, declPos, obj) + if err != nil { + return protocol.Range{}, nil, err } - // Format the type's declaration syntax. - { - // Don't duplicate comments. - spec2 := *spec - spec2.Doc = nil - spec2.Comment = nil - - var b strings.Builder - b.WriteString("type ") - fset := tokeninternal.FileSetFor(declPGF.Tok) - // TODO(adonovan): use a smarter formatter that omits - // inaccessible fields (non-exported ones from other packages). - if err := format.Node(&b, fset, &spec2); err != nil { - return protocol.Range{}, nil, err - } - typeDecl = b.String() - - // Splice in size/offset at end of first line. - // "type T struct { // size=..." - if sizeOffset != "" { - nl := strings.IndexByte(typeDecl, '\n') - if nl < 0 { - nl = len(typeDecl) - } - typeDecl = typeDecl[:nl] + " // " + sizeOffset + typeDecl[nl:] + // Splice in size/offset at end of first line. + // "type T struct { // size=..." + if sizeOffset != "" { + nl := strings.IndexByte(typeDecl, '\n') + if nl < 0 { + nl = len(typeDecl) } + typeDecl = typeDecl[:nl] + " // " + sizeOffset + typeDecl[nl:] } // Promoted fields @@ -478,7 +474,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro // already been displayed when the node was formatted // above. Don't list these again. var skip map[string]bool - if iface, ok := spec.Type.(*ast.InterfaceType); ok { + if iface, ok := spec1.Type.(*ast.InterfaceType); ok { if iface.Methods.List != nil { for _, m := range iface.Methods.List { if len(m.Names) == 1 { @@ -520,6 +516,12 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro } } + // realTypeDecl is defined to store the underlying definition of an alias. + realTypeDecl, _ := findRhsTypeDecl(ctx, snapshot, pkg, obj) // tolerate the error + if realTypeDecl != "" { + typeDecl += fmt.Sprintf("\n\n%s", realTypeDecl) + } + // Compute link data (on pkg.go.dev or other documentation host). // // If linkPath is empty, the symbol is not linkable. @@ -640,6 +642,39 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro }, nil } +// typeDeclContent returns a well formatted type definition. +func typeDeclContent(declPGF *parsego.File, declPos token.Pos, obj types.Object) (string, *ast.TypeSpec, error) { + _, spec, _ := findDeclInfo([]*ast.File{declPGF.File}, declPos) // may be nil^3 + // Don't duplicate comments. + spec1, ok := spec.(*ast.TypeSpec) + if !ok { + // We cannot find a TypeSpec for this type or alias declaration + // (that is not a type parameter or a built-in). + // This should be impossible even for ill-formed trees; + // we suspect that AST repair may be creating inconsistent + // positions. Don't report a bug in that case. (#64241) + errorf := fmt.Errorf + if !declPGF.Fixed() { + errorf = bug.Errorf + } + return "", nil, errorf("type name %q without type spec", obj.Name()) + } + spec2 := *spec1 + spec2.Doc = nil + spec2.Comment = nil + + var b strings.Builder + b.WriteString("type ") + fset := tokeninternal.FileSetFor(declPGF.Tok) + // TODO(adonovan): use a smarter formatter that omits + // inaccessible fields (non-exported ones from other packages). + if err := format.Node(&b, fset, &spec2); err != nil { + return "", nil, err + } + typeDecl := b.String() + return typeDecl, spec1, nil +} + // hoverBuiltin computes hover information when hovering over a builtin // identifier. func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*hoverResult, error) { diff --git a/gopls/internal/test/marker/testdata/definition/embed.txt b/gopls/internal/test/marker/testdata/definition/embed.txt index 8ff3e37adb3..5a29b31708f 100644 --- a/gopls/internal/test/marker/testdata/definition/embed.txt +++ b/gopls/internal/test/marker/testdata/definition/embed.txt @@ -322,6 +322,8 @@ func (a.A) Hi() -- @aAlias -- ```go type aAlias = a.A // size=16 (0x10) + +type A string ``` --- diff --git a/gopls/internal/test/marker/testdata/hover/hover_alias.txt b/gopls/internal/test/marker/testdata/hover/hover_alias.txt new file mode 100644 index 00000000000..886a175981c --- /dev/null +++ b/gopls/internal/test/marker/testdata/hover/hover_alias.txt @@ -0,0 +1,81 @@ +This test checks gopls behavior when hovering over alias type. + +-- flags -- +-skip_goarch=386,arm + +-- go.mod -- +module mod.com + +-- main.go -- +package main + +import "mod.com/a" +import "mod.com/b" + +type ToTypeDecl = b.RealType //@hover("ToTypeDecl", "ToTypeDecl", ToTypeDecl) + +type ToAlias = a.Alias //@hover("ToAlias", "ToAlias", ToAlias) + +type ToAliasWithComment = a.AliasWithComment //@hover("ToAliasWithComment", "ToAliasWithComment", ToAliasWithComment) + +-- a/a.go -- +package a +import "mod.com/b" + +type Alias = b.RealType + +// AliasWithComment is a type alias with comments. +type AliasWithComment = b.RealType + +-- b/b.go -- +package b +// RealType is a real type rather than an alias type. +type RealType struct { + Name string + Age int +} + +-- @ToTypeDecl -- +```go +type ToTypeDecl = b.RealType // size=24 (0x18) + +type RealType struct { + Name string + Age int +} +``` + +--- + +@hover("ToTypeDecl", "ToTypeDecl", ToTypeDecl) + + +--- + +[`main.ToTypeDecl` on pkg.go.dev](https://pkg.go.dev/mod.com#ToTypeDecl) +-- @ToAlias -- +```go +type ToAlias = a.Alias // size=24 (0x18) +``` + +--- + +@hover("ToAlias", "ToAlias", ToAlias) + + +--- + +[`main.ToAlias` on pkg.go.dev](https://pkg.go.dev/mod.com#ToAlias) +-- @ToAliasWithComment -- +```go +type ToAliasWithComment = a.AliasWithComment // size=24 (0x18) +``` + +--- + +@hover("ToAliasWithComment", "ToAliasWithComment", ToAliasWithComment) + + +--- + +[`main.ToAliasWithComment` on pkg.go.dev](https://pkg.go.dev/mod.com#ToAliasWithComment) From 1c52ccd39b923912d9e4b54944df219a62b60f91 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Fri, 14 Feb 2025 08:11:27 -0500 Subject: [PATCH 57/99] gopls/internal/analysis/gofix: inline most aliases Support inlining an alias with an arbitrary right-hand side. The type checker gives us almost everything we need to inline an alias; the only thing missing is the bit that says that a //go:fix directive was present. So the fact is an empty struct. Skip aliases that mention arrays. The array length expression isn't represented, and it may refer to other values, so inlining it would incorrectly decouple the inlined expression from the original. For golang/go#32816. Change-Id: I2e5ff1bd69a0f88cd7cb396dee8d4b426988d1cc Reviewed-on: https://go-review.googlesource.com/c/tools/+/650755 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/gofix/gofix.go | 220 ++++++++++++------ gopls/internal/analysis/gofix/gofix_test.go | 162 ++++++++++++- .../analysis/gofix/testdata/src/a/a.go | 56 ++++- .../analysis/gofix/testdata/src/a/a.go.golden | 57 ++++- .../gofix/testdata/src/a/internal/d.go | 2 + .../analysis/gofix/testdata/src/b/b.go | 5 + .../analysis/gofix/testdata/src/b/b.go.golden | 9 +- .../analysis/gofix/testdata/src/c/c.go | 5 + 8 files changed, 439 insertions(+), 77 deletions(-) diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index ffc64be755b..bb6ce4b43ce 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -9,6 +9,7 @@ import ( "go/ast" "go/token" "go/types" + "strings" _ "embed" @@ -118,32 +119,31 @@ func (a *analyzer) findAlias(spec *ast.TypeSpec, declInline bool) { a.pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: not a type alias") return } + + // Disallow inlines of type expressions containing array types. + // Given an array type like [N]int where N is a named constant, go/types provides + // only the value of the constant as an int64. So inlining A in this code: + // + // const N = 5 + // type A = [N]int + // + // would result in [5]int, breaking the connection with N. + // TODO(jba): accept type expressions where the array size is a literal integer + for n := range ast.Preorder(spec.Type) { + if ar, ok := n.(*ast.ArrayType); ok && ar.Len != nil { + a.pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: array types not supported") + return + } + } + if spec.TypeParams != nil { // TODO(jba): handle generic aliases return } - // The alias must refer to another named type. - // TODO(jba): generalize to more type expressions. - var rhsID *ast.Ident - switch e := ast.Unparen(spec.Type).(type) { - case *ast.Ident: - rhsID = e - case *ast.SelectorExpr: - rhsID = e.Sel - default: - return - } + + // Remember that this is an inlinable alias. + typ := &goFixInlineAliasFact{} lhs := a.pass.TypesInfo.Defs[spec.Name].(*types.TypeName) - // more (jba): test one alias pointing to another alias - rhs := a.pass.TypesInfo.Uses[rhsID].(*types.TypeName) - typ := &goFixInlineAliasFact{ - RHSName: rhs.Name(), - RHSPkgName: rhs.Pkg().Name(), - RHSPkgPath: rhs.Pkg().Path(), - } - if rhs.Pkg() == a.pass.Pkg { - typ.rhsObj = rhs - } a.inlinableAliases[lhs] = typ // Create a fact only if the LHS is exported and defined at top level. // We create a fact even if the RHS is non-exported, @@ -302,49 +302,148 @@ func (a *analyzer) inlineAlias(tn *types.TypeName, cur cursor.Cursor) { if inalias == nil { return // nope } - curFile := currentFile(cur) - // We have an identifier A here (n), possibly qualified by a package identifier (sel.X, - // where sel is the parent of X), // and an inlinable "type A = B" elsewhere (inali). - // Consider replacing A with B. + // Get the alias's RHS. It has everything we need to format the replacement text. + rhs := tn.Type().(*types.Alias).Rhs() - // Check that the expression we are inlining (B) means the same thing - // (refers to the same object) in n's scope as it does in A's scope. - // If the RHS is not in the current package, AddImport will handle - // shadowing, so we only need to worry about when both expressions - // are in the current package. + curPath := a.pass.Pkg.Path() + curFile := currentFile(cur) n := cur.Node().(*ast.Ident) - if a.pass.Pkg.Path() == inalias.RHSPkgPath { - // fcon.rhsObj is the object referred to by B in the definition of A. - scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope - _, obj := scope.LookupParent(inalias.RHSName, n.Pos()) // what "B" means in n's scope - if obj == nil { - // Should be impossible: if code at n can refer to the LHS, - // it can refer to the RHS. - panic(fmt.Sprintf("no object for inlinable alias %s RHS %s", n.Name, inalias.RHSName)) + // We have an identifier A here (n), possibly qualified by a package + // identifier (sel.n), and an inlinable "type A = rhs" elsewhere. + // + // We can replace A with rhs if no name in rhs is shadowed at n's position, + // and every package in rhs is importable by the current package. + + var ( + importPrefixes = map[string]string{curPath: ""} // from pkg path to prefix + edits []analysis.TextEdit + ) + for _, tn := range typenames(rhs) { + var pkgPath, pkgName string + if pkg := tn.Pkg(); pkg != nil { + pkgPath = pkg.Path() + pkgName = pkg.Name() } - if obj != inalias.rhsObj { - // "B" means something different here than at the inlinable const's scope. + if pkgPath == "" || pkgPath == curPath { + // The name is in the current package or the universe scope, so no import + // is required. Check that it is not shadowed (that is, that the type + // it refers to in rhs is the same one it refers to at n). + scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope + _, obj := scope.LookupParent(tn.Name(), n.Pos()) // what qn.name means in n's scope + if obj != tn { // shadowed + return + } + } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), pkgPath) { + // If this package can't see the package of this part of rhs, we can't inline. return + } else if _, ok := importPrefixes[pkgPath]; !ok { + // Use AddImport to add pkgPath if it's not there already. Associate the prefix it assigns + // with the package path for use by the TypeString qualifier below. + _, prefix, eds := analysisinternal.AddImport( + a.pass.TypesInfo, curFile, pkgName, pkgPath, tn.Name(), n.Pos()) + importPrefixes[pkgPath] = strings.TrimSuffix(prefix, ".") + edits = append(edits, eds...) } - } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), inalias.RHSPkgPath) { - // If this package can't see the RHS's package, we can't inline. - return - } - var ( - importPrefix string - edits []analysis.TextEdit - ) - if inalias.RHSPkgPath != a.pass.Pkg.Path() { - _, importPrefix, edits = analysisinternal.AddImport( - a.pass.TypesInfo, curFile, inalias.RHSPkgName, inalias.RHSPkgPath, inalias.RHSName, n.Pos()) } // If n is qualified by a package identifier, we'll need the full selector expression. var expr ast.Expr = n if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { expr = cur.Parent().Node().(ast.Expr) } - a.reportInline("type alias", "Type alias", expr, edits, importPrefix+inalias.RHSName) + // To get the replacement text, render the alias RHS using the package prefixes + // we assigned above. + newText := types.TypeString(rhs, func(p *types.Package) string { + if p == a.pass.Pkg { + return "" + } + if prefix, ok := importPrefixes[p.Path()]; ok { + return prefix + } + panic(fmt.Sprintf("in %q, package path %q has no import prefix", rhs, p.Path())) + }) + a.reportInline("type alias", "Type alias", expr, edits, newText) +} + +// typenames returns the TypeNames for types within t (including t itself) that have +// them: basic types, named types and alias types. +// The same name may appear more than once. +func typenames(t types.Type) []*types.TypeName { + var tns []*types.TypeName + + var visit func(types.Type) + + // TODO(jba): when typesinternal.NamedOrAlias adds TypeArgs, replace this type literal with it. + namedOrAlias := func(t interface { + TypeArgs() *types.TypeList + Obj() *types.TypeName + }) { + tns = append(tns, t.Obj()) + args := t.TypeArgs() + // TODO(jba): replace with TypeList.Types when this file is at 1.24. + for i := range args.Len() { + visit(args.At(i)) + } + } + + visit = func(t types.Type) { + switch t := t.(type) { + case *types.Basic: + tns = append(tns, types.Universe.Lookup(t.Name()).(*types.TypeName)) + case *types.Named: + namedOrAlias(t) + case *types.Alias: + namedOrAlias(t) + case *types.TypeParam: + tns = append(tns, t.Obj()) + case *types.Pointer: + visit(t.Elem()) + case *types.Slice: + visit(t.Elem()) + case *types.Array: + visit(t.Elem()) + case *types.Chan: + visit(t.Elem()) + case *types.Map: + visit(t.Key()) + visit(t.Elem()) + case *types.Struct: + // TODO(jba): replace with Struct.Fields when this file is at 1.24. + for i := range t.NumFields() { + visit(t.Field(i).Type()) + } + case *types.Signature: + // Ignore the receiver: although it may be present, it has no meaning + // in a type expression. + // Ditto for receiver type params. + // Also, function type params cannot appear in a type expression. + if t.TypeParams() != nil { + panic("Signature.TypeParams in type expression") + } + visit(t.Params()) + visit(t.Results()) + case *types.Interface: + for i := range t.NumEmbeddeds() { + visit(t.EmbeddedType(i)) + } + for i := range t.NumExplicitMethods() { + visit(t.ExplicitMethod(i).Type()) + } + case *types.Tuple: + // TODO(jba): replace with Tuple.Variables when this file is at 1.24. + for i := range t.Len() { + visit(t.At(i).Type()) + } + case *types.Union: + panic("Union in type expression") + default: + panic(fmt.Sprintf("unknown type %T", t)) + } + } + + visit(t) + + return tns } // If con is an inlinable constant, suggest inlining its use at cur. @@ -481,20 +580,11 @@ func (c *goFixInlineConstFact) String() string { func (*goFixInlineConstFact) AFact() {} // A goFixInlineAliasFact is exported for each type alias marked "//go:fix inline". -// It holds information about an inlinable type alias. Gob-serializable. -type goFixInlineAliasFact struct { - // Information about "type LHSName = RHSName". - RHSName string - RHSPkgPath string - RHSPkgName string - rhsObj types.Object // for current package -} - -func (c *goFixInlineAliasFact) String() string { - return fmt.Sprintf("goFixInline alias %q.%s", c.RHSPkgPath, c.RHSName) -} +// It holds no information; its mere existence demonstrates that an alias is inlinable. +type goFixInlineAliasFact struct{} -func (*goFixInlineAliasFact) AFact() {} +func (c *goFixInlineAliasFact) String() string { return "goFixInline alias" } +func (*goFixInlineAliasFact) AFact() {} func discard(string, ...any) {} diff --git a/gopls/internal/analysis/gofix/gofix_test.go b/gopls/internal/analysis/gofix/gofix_test.go index 32bd87b6cd2..dc98ef47181 100644 --- a/gopls/internal/analysis/gofix/gofix_test.go +++ b/gopls/internal/analysis/gofix/gofix_test.go @@ -2,15 +2,171 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package gofix_test +package gofix import ( + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "slices" "testing" + gocmp "github.com/google/go-cmp/cmp" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/gopls/internal/analysis/gofix" + "golang.org/x/tools/internal/testenv" ) func TestAnalyzer(t *testing.T) { - analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), gofix.Analyzer, "a", "b") + analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), Analyzer, "a", "b") +} + +func TestTypesWithNames(t *testing.T) { + // Test setup inspired by internal/analysisinternal/addimport_test.go. + testenv.NeedsDefaultImporter(t) + + for _, test := range []struct { + typeExpr string + want []string + }{ + { + "int", + []string{"int"}, + }, + { + "*int", + []string{"int"}, + }, + { + "[]*int", + []string{"int"}, + }, + { + "[2]int", + []string{"int"}, + }, + { + // go/types does not expose the length expression. + "[unsafe.Sizeof(uint(1))]int", + []string{"int"}, + }, + { + "map[string]int", + []string{"int", "string"}, + }, + { + "map[int]struct{x, y int}", + []string{"int"}, + }, + { + "T", + []string{"a.T"}, + }, + { + "iter.Seq[int]", + []string{"int", "iter.Seq"}, + }, + { + "io.Reader", + []string{"io.Reader"}, + }, + { + "map[*io.Writer]map[T]A", + []string{"a.A", "a.T", "io.Writer"}, + }, + { + "func(int, int) (bool, error)", + []string{"bool", "error", "int"}, + }, + { + "func(int, ...string) (T, *T, error)", + []string{"a.T", "error", "int", "string"}, + }, + { + "func(iter.Seq[int])", + []string{"int", "iter.Seq"}, + }, + { + "struct { a int; b bool}", + []string{"bool", "int"}, + }, + { + "struct { io.Reader; a int}", + []string{"int", "io.Reader"}, + }, + { + "map[*string]struct{x chan int; y [2]bool}", + []string{"bool", "int", "string"}, + }, + { + "interface {F(int) bool}", + []string{"bool", "int"}, + }, + { + "interface {io.Reader; F(int) bool}", + []string{"bool", "int", "io.Reader"}, + }, + { + "G", // a type parameter of the function + []string{"a.G"}, + }, + } { + src := ` + package a + import ("io"; "iter"; "unsafe") + func _(io.Reader, iter.Seq[int]) uintptr {return unsafe.Sizeof(1)} + type T int + type A = T + + func F[G any]() { + var V ` + test.typeExpr + ` + _ = V + }` + + // parse + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "a.go", src, 0) + if err != nil { + t.Errorf("%s: %v", test.typeExpr, err) + continue + } + + // type-check + info := &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Scopes: make(map[ast.Node]*types.Scope), + Defs: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + } + conf := &types.Config{ + Error: func(err error) { t.Fatalf("%s: %v", test.typeExpr, err) }, + Importer: importer.Default(), + } + pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, info) + if err != nil { + t.Errorf("%s: %v", test.typeExpr, err) + continue + } + + // Look at V's type. + typ := pkg.Scope().Lookup("F").(*types.Func). + Scope().Lookup("V").(*types.Var).Type() + tns := typenames(typ) + // Sort names for comparison. + var got []string + for _, tn := range tns { + var prefix string + if p := tn.Pkg(); p != nil && p.Path() != "" { + prefix = p.Path() + "." + } + got = append(got, prefix+tn.Name()) + } + slices.Sort(got) + got = slices.Compact(got) + + if diff := gocmp.Diff(test.want, got); diff != "" { + t.Errorf("%s: mismatch (-want, +got):\n%s", test.typeExpr, diff) + } + } } diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go b/gopls/internal/analysis/gofix/testdata/src/a/a.go index fb4d8b92172..49a0587c2b1 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/a.go +++ b/gopls/internal/analysis/gofix/testdata/src/a/a.go @@ -105,14 +105,62 @@ func shadow() { // Type aliases //go:fix inline -type A = T // want A: `goFixInline alias "a".T` +type A = T // want A: `goFixInline alias` var _ A // want `Type alias A should be inlined` -type B = []T // nope: only named RHSs - //go:fix inline -type AA = // want AA: `goFixInline alias "a".A` +type AA = // want AA: `goFixInline alias` A // want `Type alias A should be inlined` var _ AA // want `Type alias AA should be inlined` + +//go:fix inline +type ( + B = []T // want B: `goFixInline alias` + C = map[*string][]error // want C: `goFixInline alias` +) + +var _ B // want `Type alias B should be inlined` +var _ C // want `Type alias C should be inlined` + +//go:fix inline +type E = map[[Uno]string][]*T // want `invalid //go:fix inline directive: array types not supported` + +var _ E // nothing should happen here + +//go:fix inline +type F = map[internal.T]T // want F: `goFixInline alias` + +var _ F // want `Type alias F should be inlined` + +//go:fix inline +type G = []chan *internal.T // want G: `goFixInline alias` + +var _ G // want `Type alias G should be inlined` + +// local shadowing +func _() { + type string = int + const T = 1 + + var _ B // nope: B's RHS contains T, which is shadowed + var _ C // nope: C's RHS contains string, which is shadowed +} + +// local inlining +func _[P any]() { + const a = 1 + //go:fix inline + const b = a + + x := b // want `Constant b should be inlined` + + //go:fix inline + type u = []P + + var y u // want `Type alias u should be inlined` + + _ = x + _ = y +} diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden index 9ab1bcbc652..9d4c527919e 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden +++ b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden @@ -105,14 +105,63 @@ func shadow() { // Type aliases //go:fix inline -type A = T // want A: `goFixInline alias "a".T` +type A = T // want A: `goFixInline alias` var _ T // want `Type alias A should be inlined` -type B = []T // nope: only named RHSs - //go:fix inline -type AA = // want AA: `goFixInline alias "a".A` +type AA = // want AA: `goFixInline alias` T // want `Type alias A should be inlined` var _ A // want `Type alias AA should be inlined` + +//go:fix inline +type ( + B = []T // want B: `goFixInline alias` + C = map[*string][]error // want C: `goFixInline alias` +) + +var _ []T // want `Type alias B should be inlined` +var _ map[*string][]error // want `Type alias C should be inlined` + +//go:fix inline +type E = map[[Uno]string][]*T // want `invalid //go:fix inline directive: array types not supported` + +var _ E // nothing should happen here + +//go:fix inline +type F = map[internal.T]T // want F: `goFixInline alias` + +var _ map[internal.T]T // want `Type alias F should be inlined` + +//go:fix inline +type G = []chan *internal.T // want G: `goFixInline alias` + +var _ []chan *internal.T // want `Type alias G should be inlined` + +// local shadowing +func _() { + type string = int + const T = 1 + + var _ B // nope: B's RHS contains T, which is shadowed + var _ C // nope: C's RHS contains string, which is shadowed +} + + +// local inlining +func _[P any]() { + const a = 1 + //go:fix inline + const b = a + + x := a // want `Constant b should be inlined` + + //go:fix inline + type u = []P + + var y []P // want `Type alias u should be inlined` + + _ = x + _ = y +} diff --git a/gopls/internal/analysis/gofix/testdata/src/a/internal/d.go b/gopls/internal/analysis/gofix/testdata/src/a/internal/d.go index 3211d7ae3cc..60d0c1ab7e8 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/internal/d.go +++ b/gopls/internal/analysis/gofix/testdata/src/a/internal/d.go @@ -3,3 +3,5 @@ package internal const D = 1 + +type T int diff --git a/gopls/internal/analysis/gofix/testdata/src/b/b.go b/gopls/internal/analysis/gofix/testdata/src/b/b.go index d52fd514024..b358d7b4f67 100644 --- a/gopls/internal/analysis/gofix/testdata/src/b/b.go +++ b/gopls/internal/analysis/gofix/testdata/src/b/b.go @@ -32,3 +32,8 @@ func g() { const d = a.D // nope: a.D refers to a constant in a package that is not visible here. var _ a.A // want `Type alias a\.A should be inlined` +var _ a.B // want `Type alias a\.B should be inlined` +var _ a.C // want `Type alias a\.C should be inlined` +var _ R // want `Type alias R should be inlined` + +var _ a.G // nope: a.G refers to a type in a package that is not visible here diff --git a/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden b/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden index 4228ffeb489..fd8d87a2ef1 100644 --- a/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden +++ b/gopls/internal/analysis/gofix/testdata/src/b/b.go.golden @@ -2,6 +2,8 @@ package b import a0 "a" +import "io" + import ( "a" . "c" @@ -35,4 +37,9 @@ func g() { const d = a.D // nope: a.D refers to a constant in a package that is not visible here. -var _ a.T // want `Type alias a\.A should be inlined` +var _ a.T // want `Type alias a\.A should be inlined` +var _ []a.T // want `Type alias a\.B should be inlined` +var _ map[*string][]error // want `Type alias a\.C should be inlined` +var _ map[io.Reader]io.Reader // want `Type alias R should be inlined` + +var _ a.G // nope: a.G refers to a type in a package that is not visible here diff --git a/gopls/internal/analysis/gofix/testdata/src/c/c.go b/gopls/internal/analysis/gofix/testdata/src/c/c.go index 36504b886a7..7f6a3f26fe2 100644 --- a/gopls/internal/analysis/gofix/testdata/src/c/c.go +++ b/gopls/internal/analysis/gofix/testdata/src/c/c.go @@ -2,4 +2,9 @@ package c // This package is dot-imported by package b. +import "io" + const C = 1 + +//go:fix inline +type R = map[io.Reader]io.Reader From 6e3d8bca20c96bbb8297a834e4421b93e6c3ffa5 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Fri, 21 Feb 2025 11:20:11 -0500 Subject: [PATCH 58/99] gopls/internal/analysis/gofix: use 1.24 iterators For golang/go#32816. Change-Id: Icf805984f812af19c720d4f477ed12a97a5dd68d Reviewed-on: https://go-review.googlesource.com/c/tools/+/651615 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/analysis/gofix/gofix.go | 35 ++++++++++---------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index bb6ce4b43ce..237e5b0b58a 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -372,28 +372,21 @@ func typenames(t types.Type) []*types.TypeName { var tns []*types.TypeName var visit func(types.Type) - - // TODO(jba): when typesinternal.NamedOrAlias adds TypeArgs, replace this type literal with it. - namedOrAlias := func(t interface { - TypeArgs() *types.TypeList - Obj() *types.TypeName - }) { - tns = append(tns, t.Obj()) - args := t.TypeArgs() - // TODO(jba): replace with TypeList.Types when this file is at 1.24. - for i := range args.Len() { - visit(args.At(i)) - } - } - visit = func(t types.Type) { + if hasName, ok := t.(interface{ Obj() *types.TypeName }); ok { + tns = append(tns, hasName.Obj()) + } switch t := t.(type) { case *types.Basic: tns = append(tns, types.Universe.Lookup(t.Name()).(*types.TypeName)) case *types.Named: - namedOrAlias(t) + for t := range t.TypeArgs().Types() { + visit(t) + } case *types.Alias: - namedOrAlias(t) + for t := range t.TypeArgs().Types() { + visit(t) + } case *types.TypeParam: tns = append(tns, t.Obj()) case *types.Pointer: @@ -408,9 +401,8 @@ func typenames(t types.Type) []*types.TypeName { visit(t.Key()) visit(t.Elem()) case *types.Struct: - // TODO(jba): replace with Struct.Fields when this file is at 1.24. - for i := range t.NumFields() { - visit(t.Field(i).Type()) + for f := range t.Fields() { + visit(f.Type()) } case *types.Signature: // Ignore the receiver: although it may be present, it has no meaning @@ -430,9 +422,8 @@ func typenames(t types.Type) []*types.TypeName { visit(t.ExplicitMethod(i).Type()) } case *types.Tuple: - // TODO(jba): replace with Tuple.Variables when this file is at 1.24. - for i := range t.Len() { - visit(t.At(i).Type()) + for v := range t.Variables() { + visit(v.Type()) } case *types.Union: panic("Union in type expression") From 3d7c2e28a97c1a7e134dba0e3a3a27c560ddaa75 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 21 Feb 2025 21:23:18 +0000 Subject: [PATCH 59/99] gopls/internal/golang: add missing json tags for hoverResult In my haste to partially revert CL 635226 in 651238, I failed to add json struct tags. Add them back. For golang/go#71879 Change-Id: I45190cba5154eeed7b6a49db51d2a2a51999be7a Reviewed-on: https://go-review.googlesource.com/c/tools/+/651618 Auto-Submit: Robert Findley Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/hover.go | 14 +++++++------- gopls/internal/test/marker/testdata/hover/json.txt | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 947595715a7..74cf5dbb593 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -57,20 +57,20 @@ type hoverResult struct { // TODO(adonovan): in what syntax? It (usually) comes from doc.Synopsis, // which produces "Text" form, but it may be fed to // DocCommentToMarkdown, which expects doc comment syntax. - Synopsis string + Synopsis string `json:"synopsis"` // FullDocumentation is the symbol's full documentation. - FullDocumentation string + FullDocumentation string `json:"fullDocumentation"` // Signature is the symbol's Signature. - Signature string + Signature string `json:"signature"` // SingleLine is a single line describing the symbol. // This is recommended only for use in clients that show a single line for hover. - SingleLine string + SingleLine string `json:"singleLine"` // SymbolName is the human-readable name to use for the symbol in links. - SymbolName string + SymbolName string `json:"symbolName"` // LinkPath is the path of the package enclosing the given symbol, // with the module portion (if any) replaced by "module@version". @@ -78,11 +78,11 @@ type hoverResult struct { // For example: "github.com/google/go-github/v48@v48.1.0/github". // // Use LinkTarget + "/" + LinkPath + "#" + LinkAnchor to form a pkgsite URL. - LinkPath string + LinkPath string `json:"linkPath"` // LinkAnchor is the pkg.go.dev link anchor for the given symbol. // For example, the "Node" part of "pkg.go.dev/go/ast#Node". - LinkAnchor string + LinkAnchor string `json:"linkAnchor"` // New fields go below, and are unexported. The existing // exported fields are underspecified and have already diff --git a/gopls/internal/test/marker/testdata/hover/json.txt b/gopls/internal/test/marker/testdata/hover/json.txt index 6c489cb4221..f3229805cb6 100644 --- a/gopls/internal/test/marker/testdata/hover/json.txt +++ b/gopls/internal/test/marker/testdata/hover/json.txt @@ -28,6 +28,6 @@ func MyFunc(i int) string { //@ hover("MyFunc", "MyFunc", MyFunc) return "" } -- @MyFunc -- -{"Synopsis":"MyFunc is a function.","FullDocumentation":"MyFunc is a function.\n","Signature":"func MyFunc(i int) string","SingleLine":"func MyFunc(i int) string","SymbolName":"p.MyFunc","LinkPath":"example.com/p","LinkAnchor":"MyFunc"} +{"synopsis":"MyFunc is a function.","fullDocumentation":"MyFunc is a function.\n","signature":"func MyFunc(i int) string","singleLine":"func MyFunc(i int) string","symbolName":"p.MyFunc","linkPath":"example.com/p","linkAnchor":"MyFunc"} -- @MyType -- -{"Synopsis":"MyType is a type.","FullDocumentation":"MyType is a type.\n","Signature":"type MyType struct { // size=24 (0x18)\n\tF int // a field\n\tS string // a string field\n}\n","SingleLine":"type MyType struct{F int; S string}","SymbolName":"p.MyType","LinkPath":"example.com/p","LinkAnchor":"MyType"} +{"synopsis":"MyType is a type.","fullDocumentation":"MyType is a type.\n","signature":"type MyType struct { // size=24 (0x18)\n\tF int // a field\n\tS string // a string field\n}\n","singleLine":"type MyType struct{F int; S string}","symbolName":"p.MyType","linkPath":"example.com/p","linkAnchor":"MyType"} From 5299dcb7277190caeca1a827cb7d5c856b22f37f Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 21 Feb 2025 21:57:05 +0000 Subject: [PATCH 60/99] gopls/internal/settings: fix misleading error messages The deprecatedError helper constructs a specifically formatted error string suggesting a replacement. Certain deprecations were misusing the API, resulting in nonsensical error messages. Change-Id: Ic72bf608b5b2e97baf75c192a49fd4181d7800b2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651695 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/settings/settings.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 7b04e6b746b..dd353da64e9 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -1116,7 +1116,7 @@ func (o *Options) setOne(name string, value any) (applied []CounterPath, _ error return nil, err } if o.Analyses["fieldalignment"] { - return counts, deprecatedError("the 'fieldalignment' analyzer was removed in gopls/v0.17.0; instead, hover over struct fields to see size/offset information (https://go.dev/issue/66861)") + return counts, &SoftError{"the 'fieldalignment' analyzer was removed in gopls/v0.17.0; instead, hover over struct fields to see size/offset information (https://go.dev/issue/66861)"} } return counts, nil @@ -1124,7 +1124,7 @@ func (o *Options) setOne(name string, value any) (applied []CounterPath, _ error return setBoolMap(&o.Hints, value) case "annotations": - return nil, deprecatedError("the 'annotations' setting was removed in gopls/v0.18.0; all compiler optimization details are now shown") + return nil, &SoftError{"the 'annotations' setting was removed in gopls/v0.18.0; all compiler optimization details are now shown"} case "vulncheck": return setEnum(&o.Vulncheck, value, From 274b2375098fb4ba49aedcc8b86edcbf2079ba0a Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 21 Feb 2025 20:26:19 +0000 Subject: [PATCH 61/99] gopls: add a -severity flag for gopls check In golang/go#50764, users were reporting having to filter out noisy diagnostics from the output of `gopls check` in CI. This is because there was no differentiation between diagnostics that represent real bugs, and those that are suggestions. By contrast, hint level diagnostics are very unobtrusive in the editor. Add a new -severity flag to control the minimum severity output by gopls check, and set its default to "warning". For golang/go#50764 Change-Id: I48d8bb74371fa6035fef4d2608412b986f509f7b Reviewed-on: https://go-review.googlesource.com/c/tools/+/651616 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Auto-Submit: Robert Findley --- gopls/doc/release/v0.19.0.md | 7 +++++++ gopls/internal/cmd/check.go | 20 +++++++++++++++++++- gopls/internal/cmd/cmd.go | 2 +- gopls/internal/cmd/integration_test.go | 22 ++++++++++++++++++++++ gopls/internal/cmd/usage/check.hlp | 2 ++ 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/gopls/doc/release/v0.19.0.md b/gopls/doc/release/v0.19.0.md index 0b3ea64c305..18088732656 100644 --- a/gopls/doc/release/v0.19.0.md +++ b/gopls/doc/release/v0.19.0.md @@ -1,3 +1,10 @@ +# Configuration Changes + +- The `gopls check` subcommant now accepts a `-severity` flag to set a minimum + severity for the diagnostics it reports. By default, the minimum severity + is "warning", so `gopls check` may report fewer diagnostics than before. Set + `-severity=hint` to reproduce the previous behavior. + # New features ## "Eliminate dot import" code action diff --git a/gopls/internal/cmd/check.go b/gopls/internal/cmd/check.go index d256fa9de2a..8c0362b148a 100644 --- a/gopls/internal/cmd/check.go +++ b/gopls/internal/cmd/check.go @@ -16,7 +16,8 @@ import ( // check implements the check verb for gopls. type check struct { - app *Application + app *Application + Severity string `flag:"severity" help:"minimum diagnostic severity (hint, info, warning, or error)"` } func (c *check) Name() string { return "check" } @@ -35,6 +36,20 @@ Example: show the diagnostic results of this file: // Run performs the check on the files specified by args and prints the // results to stdout. func (c *check) Run(ctx context.Context, args ...string) error { + severityCutoff := protocol.SeverityWarning + switch c.Severity { + case "hint": + severityCutoff = protocol.SeverityHint + case "info": + severityCutoff = protocol.SeverityInformation + case "warning": + // default + case "error": + severityCutoff = protocol.SeverityError + default: + return fmt.Errorf("unrecognized -severity value %q", c.Severity) + } + if len(args) == 0 { return nil } @@ -95,6 +110,9 @@ func (c *check) Run(ctx context.Context, args ...string) error { file.diagnosticsMu.Unlock() for _, diag := range diags { + if diag.Severity > severityCutoff { // lower severity value => greater severity, counterintuitively + continue + } if err := print(file.uri, diag.Range, diag.Message); err != nil { return err } diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go index 8bd7d7b899f..119577c012b 100644 --- a/gopls/internal/cmd/cmd.go +++ b/gopls/internal/cmd/cmd.go @@ -284,7 +284,7 @@ func (app *Application) internalCommands() []tool.Application { func (app *Application) featureCommands() []tool.Application { return []tool.Application{ &callHierarchy{app: app}, - &check{app: app}, + &check{app: app, Severity: "warning"}, &codeaction{app: app}, &codelens{app: app}, &definition{app: app}, diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index 986453253f8..e7ac774f5c0 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go @@ -108,6 +108,12 @@ var C int -- c/c2.go -- package c var C int +-- d/d.go -- +package d + +import "io/ioutil" + +var _ = ioutil.ReadFile `) // no files @@ -141,6 +147,22 @@ var C int res.checkStdout(`c2.go:2:5-6: C redeclared in this block`) res.checkStdout(`c.go:2:5-6: - other declaration of C`) } + + // No deprecated (hint) diagnostic without -severity. + { + res := gopls(t, tree, "check", "./d/d.go") + res.checkExit(true) + if len(res.stdout) > 0 { + t.Errorf("check ./d/d.go returned unexpected output:\n%s", res.stdout) + } + } + + // Deprecated (hint) diagnostics with -severity=hint + { + res := gopls(t, tree, "check", "-severity=hint", "./d/d.go") + res.checkExit(true) + res.checkStdout(`ioutil.ReadFile is deprecated`) + } } // TestCallHierarchy tests the 'call_hierarchy' subcommand (call_hierarchy.go). diff --git a/gopls/internal/cmd/usage/check.hlp b/gopls/internal/cmd/usage/check.hlp index eda1a25a191..c387c2cf5d9 100644 --- a/gopls/internal/cmd/usage/check.hlp +++ b/gopls/internal/cmd/usage/check.hlp @@ -6,3 +6,5 @@ Usage: Example: show the diagnostic results of this file: $ gopls check internal/cmd/check.go + -severity=string + minimum diagnostic severity (hint, info, warning, or error) (default "warning") From 739a5af40476496b626dc23e996357a7dff4e3e8 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Sun, 23 Feb 2025 13:08:20 +0000 Subject: [PATCH 62/99] gopls/internal/test/marker: skip on the freebsd race builder The marker tests frequently time out on the freebsd race builder. Skip them to reduce noise. (We don't currently have resources to investigate). Fixes golang/go#71731 Change-Id: I2e27c2e8063b6d5e698eb9d1b5c32d08914fcc77 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651895 Auto-Submit: Robert Findley Reviewed-by: Peter Weinberger LUCI-TryBot-Result: Go LUCI --- gopls/internal/test/marker/marker_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index d7f91abed46..a5e23b928ad 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -96,6 +96,9 @@ func Test(t *testing.T) { if strings.HasPrefix(builder, "darwin-") || strings.Contains(builder, "solaris") { t.Skip("golang/go#64473: skipping with -short: this test is too slow on darwin and solaris builders") } + if strings.HasSuffix(builder, "freebsd-amd64-race") { + t.Skip("golang/go#71731: the marker tests are too slow to run on the amd64-race builder") + } } // The marker tests must be able to run go/packages.Load. testenv.NeedsGoPackages(t) @@ -658,7 +661,7 @@ type stringListValue []string func (l *stringListValue) Set(s string) error { if s != "" { - for _, d := range strings.Split(s, ",") { + for d := range strings.SplitSeq(s, ",") { *l = append(*l, strings.TrimSpace(d)) } } @@ -1838,7 +1841,7 @@ func removeDiagnostic(mark marker, loc protocol.Location, matchEnd bool, re *reg diags := mark.run.diags[key] for i, diag := range diags { if re.MatchString(diag.Message) && (!matchEnd || diag.Range.End == loc.Range.End) { - mark.run.diags[key] = append(diags[:i], diags[i+1:]...) + mark.run.diags[key] = slices.Delete(diags, i, i+1) return diag, true } } From 2b2a44ed6f269fbfd9adfd17139a0485e1b0a144 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Sun, 23 Feb 2025 13:52:01 +0000 Subject: [PATCH 63/99] gopls/internal/test: avoid panic in TestDoubleParamReturnCompletion An invalid assumption in this test led to an out of bounds error, which likely masked a real error or timeout. Update the test to not panic, and factor. Fixes golang/go#71906 Change-Id: Ib01d3b75df8bdb71984457807312cfe1d27ddf73 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651896 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- .../integration/completion/completion_test.go | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/gopls/internal/test/integration/completion/completion_test.go b/gopls/internal/test/integration/completion/completion_test.go index 1d293fe9019..0713b1f62b9 100644 --- a/gopls/internal/test/integration/completion/completion_test.go +++ b/gopls/internal/test/integration/completion/completion_test.go @@ -1212,25 +1212,21 @@ func TestDoubleParamReturnCompletion(t *testing.T) { Run(t, src, func(t *testing.T, env *Env) { env.OpenFile("a.go") - compl := env.RegexpSearch("a.go", `DoubleWrap\[()\]\(\)`) - result := env.Completion(compl) - - wantLabel := []string{"InterfaceA", "TypeA", "InterfaceB", "TypeB", "TypeC"} - - for i, item := range result.Items[:len(wantLabel)] { - if diff := cmp.Diff(wantLabel[i], item.Label); diff != "" { - t.Errorf("Completion: unexpected label mismatch (-want +got):\n%s", diff) - } + tests := map[string][]string{ + `DoubleWrap\[()\]\(\)`: {"InterfaceA", "TypeA", "InterfaceB", "TypeB", "TypeC"}, + `DoubleWrap\[InterfaceA, (_)\]\(\)`: {"InterfaceB", "TypeB", "TypeX", "InterfaceA", "TypeA"}, } - compl = env.RegexpSearch("a.go", `DoubleWrap\[InterfaceA, (_)\]\(\)`) - result = env.Completion(compl) - - wantLabel = []string{"InterfaceB", "TypeB", "TypeX", "InterfaceA", "TypeA"} - - for i, item := range result.Items[:len(wantLabel)] { - if diff := cmp.Diff(wantLabel[i], item.Label); diff != "" { - t.Errorf("Completion: unexpected label mismatch (-want +got):\n%s", diff) + for re, wantLabels := range tests { + compl := env.RegexpSearch("a.go", re) + result := env.Completion(compl) + if len(result.Items) < len(wantLabels) { + t.Fatalf("Completion(%q) returned mismatching labels: got %v, want at least labels %v", re, result.Items, wantLabels) + } + for i, item := range result.Items[:len(wantLabels)] { + if diff := cmp.Diff(wantLabels[i], item.Label); diff != "" { + t.Errorf("Completion(%q): unexpected label mismatch (-want +got):\n%s", re, diff) + } } } }) From d2fcd360ffaa3fcc4c225918750054b056033d3c Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 20 Feb 2025 16:31:14 -0500 Subject: [PATCH 64/99] go/analysis/passes/unreachable/testdata: relax test for CL 638395 This test case is about to become a parse error. To allow us to submit the change to the parser, we must relax this test. Updates golang/go#71659 Updates golang/go#70957 Change-Id: Ic4fbfedb69d152d691dec41a94bb402149463f84 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651155 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/analysis/passes/unreachable/testdata/src/a/a.go | 5 ----- go/analysis/passes/unreachable/testdata/src/a/a.go.golden | 5 ----- 2 files changed, 10 deletions(-) diff --git a/go/analysis/passes/unreachable/testdata/src/a/a.go b/go/analysis/passes/unreachable/testdata/src/a/a.go index b283fd00b9a..136a07caa21 100644 --- a/go/analysis/passes/unreachable/testdata/src/a/a.go +++ b/go/analysis/passes/unreachable/testdata/src/a/a.go @@ -2118,11 +2118,6 @@ var _ = func() int { println() // ok } -var _ = func() { - // goto without label used to panic - goto -} - func _() int { // Empty switch tag with non-bool case value used to panic. switch { diff --git a/go/analysis/passes/unreachable/testdata/src/a/a.go.golden b/go/analysis/passes/unreachable/testdata/src/a/a.go.golden index 40494030423..79cb89d4181 100644 --- a/go/analysis/passes/unreachable/testdata/src/a/a.go.golden +++ b/go/analysis/passes/unreachable/testdata/src/a/a.go.golden @@ -2082,11 +2082,6 @@ var _ = func() int { println() // ok } -var _ = func() { - // goto without label used to panic - goto -} - func _() int { // Empty switch tag with non-bool case value used to panic. switch { From 3e76cae71578160dca62d1cab42a715ef960c892 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 20 Feb 2025 16:37:02 -0500 Subject: [PATCH 65/99] internal/analysisinternal: ValidateFix: more specific errors These details help us diagnose errors in gopls especially relating to synthezized End() positions beyond EOF. Change-Id: Iff36f5c4e01f2256f2cbf8cc03b27d7b3aa74b11 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651097 Reviewed-by: Robert Findley Commit-Queue: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- go/analysis/internal/checker/fix_test.go | 4 ++-- internal/analysisinternal/analysis.go | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/go/analysis/internal/checker/fix_test.go b/go/analysis/internal/checker/fix_test.go index 68d965d08d6..00710cc0e1b 100644 --- a/go/analysis/internal/checker/fix_test.go +++ b/go/analysis/internal/checker/fix_test.go @@ -93,7 +93,7 @@ func TestReportInvalidDiagnostic(t *testing.T) { // TextEdit has invalid Pos. { "bad Pos", - `analyzer "a" suggests invalid fix .*: missing file info for pos`, + `analyzer "a" suggests invalid fix .*: no token.File for TextEdit.Pos .0.`, func(pos token.Pos) analysis.Diagnostic { return analysis.Diagnostic{ Pos: pos, @@ -110,7 +110,7 @@ func TestReportInvalidDiagnostic(t *testing.T) { // TextEdit has invalid End. { "End < Pos", - `analyzer "a" suggests invalid fix .*: pos .* > end`, + `analyzer "a" suggests invalid fix .*: TextEdit.Pos .* > TextEdit.End .*`, func(pos token.Pos) analysis.Diagnostic { return analysis.Diagnostic{ Pos: pos, diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index aba435fa404..5eb7ac5a939 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -419,18 +419,19 @@ func validateFix(fset *token.FileSet, fix *analysis.SuggestedFix) error { start := edit.Pos file := fset.File(start) if file == nil { - return fmt.Errorf("missing file info for pos (%v)", edit.Pos) + return fmt.Errorf("no token.File for TextEdit.Pos (%v)", edit.Pos) } if end := edit.End; end.IsValid() { if end < start { - return fmt.Errorf("pos (%v) > end (%v)", edit.Pos, edit.End) + return fmt.Errorf("TextEdit.Pos (%v) > TextEdit.End (%v)", edit.Pos, edit.End) } endFile := fset.File(end) if endFile == nil { - return fmt.Errorf("malformed end position %v", end) + return fmt.Errorf("no token.File for TextEdit.End (%v; File(start).FileEnd is %d)", end, file.Base()+file.Size()) } if endFile != file { - return fmt.Errorf("edit spans files %v and %v", file.Name(), endFile.Name()) + return fmt.Errorf("edit #%d spans files (%v and %v)", + i, file.Position(edit.Pos), endFile.Position(edit.End)) } } else { edit.End = start // update the SuggestedFix From 851c747e148e33b095d285e569c734385d2b074b Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 24 Feb 2025 15:05:15 +0000 Subject: [PATCH 66/99] gopls/internal/golang: fix crash when hovering over implicit After months of intermittent investigation, I was finally able to reproduce the telemetry crash of golang/go#69362: if the operant of the type switch is undefined, the selectedType will be nil, and therefore logic may proceed to the point where it reaches a nil entry in types.Info.Defs. The fix is of course straightforward, now that we understand it: we cannot rely on types.Info.Defs not containing nil entries. A follow-up CL will introduce an analyzer to detect such problematic uses of the go/types API. Fixes golang/go#69362 Change-Id: I8f75c24710dbb2e78c79d8b9d721f45d9a040cd7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/652015 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/hover.go | 13 ++++--------- .../internal/test/marker/testdata/hover/issues.txt | 12 ++++++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 74cf5dbb593..c3fecd1c9d1 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -375,15 +375,10 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro // use the default build config for all other types, even // if they embed platform-variant types. // - var sizeOffset string // optional size/offset description - // debugging #69362: unexpected nil Defs[ident] value (?) - _ = ident.Pos() // (can't be nil due to check after referencedObject) - _ = pkg.TypesInfo() // (can't be nil due to check in call to inferredSignature) - _ = pkg.TypesInfo().Defs // (can't be nil due to nature of cache.Package) - if def, ok := pkg.TypesInfo().Defs[ident]; ok { - _ = def.Pos() // can't be nil due to reasoning in #69362. - } - if def, ok := pkg.TypesInfo().Defs[ident]; ok && ident.Pos() == def.Pos() { + var sizeOffset string + + // As painfully learned in golang/go#69362, Defs can contain nil entries. + if def, _ := pkg.TypesInfo().Defs[ident]; def != nil && ident.Pos() == def.Pos() { // This is the declaring identifier. // (We can't simply use ident.Pos() == obj.Pos() because // referencedObject prefers the TypeName for an embedded field). diff --git a/gopls/internal/test/marker/testdata/hover/issues.txt b/gopls/internal/test/marker/testdata/hover/issues.txt index 6212964dff2..eda0eea3efa 100644 --- a/gopls/internal/test/marker/testdata/hover/issues.txt +++ b/gopls/internal/test/marker/testdata/hover/issues.txt @@ -20,3 +20,15 @@ package issue64237 import "golang.org/lsptests/nonexistant" //@diag("\"golang", re"could not import") var _ = nonexistant.Value //@hovererr("nonexistant", "no package data") + +-- issue69362/p.go -- +package issue69362 + +// golang/go#69362: hover panics over undefined implicits. + +func _() { + switch x := y.(type) { //@diag("y", re"undefined"), hover("x", "x", "") + case int: + _ = x + } +} From bf9e2a812de33f4ff08ed99be3ecfa95d857830e Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Thu, 13 Feb 2025 12:49:36 -0500 Subject: [PATCH 67/99] gopls/internal: test fixes for some imports bugs The CL has tests for two fixed bugs. For the new imports to work the metadata graph has to be current, which in this CL, is accomplished with a call to snapshot.WordspaceMetadata which may wait for changes to be assimilated. Fixes: golang.go/go#44510 Fixes: golang.go/go#67973 Change-Id: Ieb5a9361a75796a172da953cc58853d38f596ebd Reviewed-on: https://go-review.googlesource.com/c/tools/+/649315 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/source.go | 2 +- gopls/internal/golang/format.go | 3 + .../test/integration/misc/imports_test.go | 55 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/gopls/internal/cache/source.go b/gopls/internal/cache/source.go index fa038ec37a6..7946b9746ab 100644 --- a/gopls/internal/cache/source.go +++ b/gopls/internal/cache/source.go @@ -212,7 +212,7 @@ func (s *goplsSource) resolveWorkspaceReferences(filename string, missing import // keep track of used syms and found results by package name // TODO: avoid import cycles (is current package in forward closure) founds := make(map[string][]found) - for i := 0; i < len(ids); i++ { + for i := range len(ids) { nm := string(pkgs[i].Name) if satisfies(syms[i], missing[nm]) { got := &imports.Result{ diff --git a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go index acc619eba0c..b353538d978 100644 --- a/gopls/internal/golang/format.go +++ b/gopls/internal/golang/format.go @@ -152,6 +152,9 @@ func computeImportEdits(ctx context.Context, pgf *parsego.File, snapshot *cache. case settings.ImportsSourceGoimports: source = isource } + // imports require a current metadata graph + // TODO(rfindlay) improve the API + snapshot.WorkspaceMetadata(ctx) allFixes, err := imports.FixImports(ctx, filename, pgf.Src, goroot, options.Env.Logf, source) if err != nil { return nil, nil, err diff --git a/gopls/internal/test/integration/misc/imports_test.go b/gopls/internal/test/integration/misc/imports_test.go index 98a70478ecf..bcbfacc967a 100644 --- a/gopls/internal/test/integration/misc/imports_test.go +++ b/gopls/internal/test/integration/misc/imports_test.go @@ -401,6 +401,31 @@ return nil } }) } + +// use the import from a different package in the same module +func Test44510(t *testing.T) { + const files = `-- go.mod -- +module test +go 1.19 +-- foo/foo.go -- +package main +import strs "strings" +var _ = strs.Count +-- bar/bar.go -- +package main +var _ = strs.Builder +` + WithOptions( + WriteGoSum("."), + ).Run(t, files, func(T *testing.T, env *Env) { + env.OpenFile("bar/bar.go") + env.SaveBuffer("bar/bar.go") + buf := env.BufferText("bar/bar.go") + if !strings.Contains(buf, "strs") { + t.Error(buf) + } + }) +} func TestRelativeReplace(t *testing.T) { const files = ` -- go.mod -- @@ -688,3 +713,33 @@ func Test() { } }) } + +// this test replaces 'package bar' with 'package foo' +// saves the file, and then looks for the import in the main package.s +func Test67973(t *testing.T) { + const files = `-- go.mod -- +module hello +go 1.19 +-- hello.go -- +package main +var _ = foo.Bar +-- internal/foo/foo.go -- +package bar +func Bar() {} +` + WithOptions( + Settings{"importsSource": settings.ImportsSourceGopls}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("hello.go") + env.AfterChange(env.DoneWithOpen()) + env.SaveBuffer("hello.go") + env.OpenFile("internal/foo/foo.go") + env.RegexpReplace("internal/foo/foo.go", "bar", "foo") + env.SaveBuffer("internal/foo/foo.go") + env.SaveBuffer("hello.go") + buf := env.BufferText("hello.go") + if !strings.Contains(buf, "internal/foo") { + t.Errorf(`expected import "hello/internal/foo" but got %q`, buf) + } + }) +} From 6d4af1e1f521077aa5f196b9a4be4297d95ec1c2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 24 Feb 2025 17:24:53 -0500 Subject: [PATCH 68/99] gopls/internal/golang: Assembly: update "Compiling" message This CL causes the "Browse assembly" feature to flush the header early, and to update the "Compiling..." message when the report is complete. Change-Id: I96e0c3e1e0949dadbbd058101959f01e38e7596b Reviewed-on: https://go-review.googlesource.com/c/tools/+/652196 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/golang/assembly.go | 60 ++++++++++++++++++++----------- gopls/internal/server/server.go | 7 +--- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/gopls/internal/golang/assembly.go b/gopls/internal/golang/assembly.go index 3b778a54697..a74c48a171d 100644 --- a/gopls/internal/golang/assembly.go +++ b/gopls/internal/golang/assembly.go @@ -16,6 +16,8 @@ import ( "context" "fmt" "html" + "io" + "net/http" "regexp" "strconv" "strings" @@ -26,39 +28,33 @@ import ( // AssemblyHTML returns an HTML document containing an assembly listing of the selected function. // -// TODO(adonovan): -// - display a "Compiling..." message as a cold build can be slow. -// - cross-link jumps and block labels, like github.com/aclements/objbrowse. -func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string, web Web) ([]byte, error) { - // Compile the package with -S, and capture its stderr stream. +// TODO(adonovan): cross-link jumps and block labels, like github.com/aclements/objbrowse. +func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, w http.ResponseWriter, pkg *cache.Package, symbol string, web Web) { + // Prepare to compile the package with -S, and capture its stderr stream. inv, cleanupInvocation, err := snapshot.GoCommandInvocation(cache.NoNetwork, pkg.Metadata().CompiledGoFiles[0].DirPath(), "build", []string{"-gcflags=-S", "."}) if err != nil { - return nil, err // e.g. failed to write overlays (rare) + // e.g. failed to write overlays (rare) + http.Error(w, err.Error(), http.StatusInternalServerError) + return } defer cleanupInvocation() - _, stderr, err, _ := snapshot.View().GoCommandRunner().RunRaw(ctx, *inv) - if err != nil { - return nil, err // e.g. won't compile - } - content := stderr.String() escape := html.EscapeString - // Produce the report. + // Emit the start of the report. title := fmt.Sprintf("%s assembly for %s", escape(snapshot.View().GOARCH()), escape(symbol)) - var buf bytes.Buffer - buf.WriteString(` + io.WriteString(w, ` - ` + escape(title) + ` + `+escape(title)+` -

` + title + `

+

`+title+`

A Quick Guide to Go's Assembler

@@ -69,11 +65,23 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pack Click on a source line marker L1234 to navigate your editor there. (VS Code users: please upvote #208093)

-

- Reload the page to recompile. -

+

Compiling...

 `)
+	if flusher, ok := w.(http.Flusher); ok {
+		flusher.Flush()
+	}
+
+	// Compile the package.
+	_, stderr, err, _ := snapshot.View().GoCommandRunner().RunRaw(ctx, *inv)
+	if err != nil {
+		// e.g. won't compile
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	// Write the rest of the report.
+	content := stderr.String()
 
 	// insnRx matches an assembly instruction line.
 	// Submatch groups are: (offset-hex-dec, file-line-column, instruction).
@@ -88,7 +96,8 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pack
 	//
 	// Allow matches of symbol, symbol.func1, symbol.deferwrap, etc.
 	on := false
-	for _, line := range strings.Split(content, "\n") {
+	var buf bytes.Buffer
+	for line := range strings.SplitSeq(content, "\n") {
 		// start of function symbol?
 		if strings.Contains(line, " STEXT ") {
 			on = strings.HasPrefix(line, symbol) &&
@@ -116,5 +125,14 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pack
 		}
 		buf.WriteByte('\n')
 	}
-	return buf.Bytes(), nil
+
+	// Update the "Compiling..." message.
+	buf.WriteString(`
+
+ +`) + + w.Write(buf.Bytes()) } diff --git a/gopls/internal/server/server.go b/gopls/internal/server/server.go index d9090250a66..033295ffb32 100644 --- a/gopls/internal/server/server.go +++ b/gopls/internal/server/server.go @@ -447,12 +447,7 @@ func (s *server) initWeb() (*web, error) { pkg := pkgs[0] // Produce report. - html, err := golang.AssemblyHTML(ctx, snapshot, pkg, symbol, web) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(html) + golang.AssemblyHTML(ctx, snapshot, w, pkg, symbol, web) }) return web, nil From e890c1f6805a34b15289937191b324a5172f9c22 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 24 Feb 2025 18:03:04 -0500 Subject: [PATCH 69/99] gopls/internal/golang: Assembly: support package level var and init This CL offers the "Browse Assembly" code action when the selection is within a package-level var initializer, or a source-level init function. + Test Change-Id: Ic0fcf321765027df0c11fb7269a1aedf971814fc Reviewed-on: https://go-review.googlesource.com/c/tools/+/652197 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/golang/assembly.go | 32 ++--- gopls/internal/golang/codeaction.go | 86 +++++++++----- .../test/integration/misc/webserver_test.go | 109 ++++++++++++------ 3 files changed, 144 insertions(+), 83 deletions(-) diff --git a/gopls/internal/golang/assembly.go b/gopls/internal/golang/assembly.go index a74c48a171d..9e673dd9719 100644 --- a/gopls/internal/golang/assembly.go +++ b/gopls/internal/golang/assembly.go @@ -29,6 +29,8 @@ import ( // AssemblyHTML returns an HTML document containing an assembly listing of the selected function. // // TODO(adonovan): cross-link jumps and block labels, like github.com/aclements/objbrowse. +// +// See gopls/internal/test/integration/misc/webserver_test.go for tests. func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, w http.ResponseWriter, pkg *cache.Package, symbol string, web Web) { // Prepare to compile the package with -S, and capture its stderr stream. inv, cleanupInvocation, err := snapshot.GoCommandInvocation(cache.NoNetwork, pkg.Metadata().CompiledGoFiles[0].DirPath(), "build", []string{"-gcflags=-S", "."}) @@ -72,11 +74,26 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, w http.Response flusher.Flush() } + // At this point errors must be reported by writing HTML. + // To do this, set "status" return early. + + var buf bytes.Buffer + status := "Reload the page to recompile." + defer func() { + // Update the "Compiling..." message. + fmt.Fprintf(&buf, ` + + +`, status) + w.Write(buf.Bytes()) + }() + // Compile the package. _, stderr, err, _ := snapshot.View().GoCommandRunner().RunRaw(ctx, *inv) if err != nil { - // e.g. won't compile - http.Error(w, err.Error(), http.StatusInternalServerError) + status = fmt.Sprintf("compilation failed: %v", err) return } @@ -96,7 +113,6 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, w http.Response // // Allow matches of symbol, symbol.func1, symbol.deferwrap, etc. on := false - var buf bytes.Buffer for line := range strings.SplitSeq(content, "\n") { // start of function symbol? if strings.Contains(line, " STEXT ") { @@ -125,14 +141,4 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, w http.Response } buf.WriteByte('\n') } - - // Update the "Compiling..." message. - buf.WriteString(` - - -`) - - w.Write(buf.Bytes()) } diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 587ae3e2de3..74f3c2b6085 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -945,44 +945,66 @@ func goAssembly(ctx context.Context, req *codeActionsRequest) error { // directly to (say) a lambda of interest. // Perhaps we could scroll to STEXT for the innermost // enclosing nested function? - path, _ := astutil.PathEnclosingInterval(req.pgf.File, req.start, req.end) - if len(path) >= 2 { // [... FuncDecl File] - if decl, ok := path[len(path)-2].(*ast.FuncDecl); ok { - if fn, ok := req.pkg.TypesInfo().Defs[decl.Name].(*types.Func); ok { - sig := fn.Signature() - - // Compute the linker symbol of the enclosing function. - var sym strings.Builder - if fn.Pkg().Name() == "main" { - sym.WriteString("main") - } else { - sym.WriteString(fn.Pkg().Path()) - } - sym.WriteString(".") - if sig.Recv() != nil { - if isPtr, named := typesinternal.ReceiverNamed(sig.Recv()); named != nil { - if isPtr { - fmt.Fprintf(&sym, "(*%s)", named.Obj().Name()) - } else { - sym.WriteString(named.Obj().Name()) + + // Compute the linker symbol of the enclosing function or var initializer. + var sym strings.Builder + if pkg := req.pkg.Types(); pkg.Name() == "main" { + sym.WriteString("main") + } else { + sym.WriteString(pkg.Path()) + } + sym.WriteString(".") + + curSel, _ := req.pgf.Cursor.FindPos(req.start, req.end) + for cur := range curSel.Ancestors((*ast.FuncDecl)(nil), (*ast.ValueSpec)(nil)) { + var name string // in command title + switch node := cur.Node().(type) { + case *ast.FuncDecl: + // package-level func or method + if fn, ok := req.pkg.TypesInfo().Defs[node.Name].(*types.Func); ok && + fn.Name() != "_" { // blank functions are not compiled + + // Source-level init functions are compiled (along with + // package-level var initializers) in into a single pkg.init + // function, so this falls out of the logic below. + + if sig := fn.Signature(); sig.TypeParams() == nil && sig.RecvTypeParams() == nil { // generic => no assembly + if sig.Recv() != nil { + if isPtr, named := typesinternal.ReceiverNamed(sig.Recv()); named != nil { + if isPtr { + fmt.Fprintf(&sym, "(*%s)", named.Obj().Name()) + } else { + sym.WriteString(named.Obj().Name()) + } + sym.WriteByte('.') } - sym.WriteByte('.') } + sym.WriteString(fn.Name()) + + name = node.Name.Name // success } - sym.WriteString(fn.Name()) - - if fn.Name() != "_" && // blank functions are not compiled - (fn.Name() != "init" || sig.Recv() != nil) && // init functions aren't linker functions - sig.TypeParams() == nil && sig.RecvTypeParams() == nil { // generic => no assembly - cmd := command.NewAssemblyCommand( - fmt.Sprintf("Browse %s assembly for %s", view.GOARCH(), decl.Name), - view.ID(), - string(req.pkg.Metadata().ID), - sym.String()) - req.addCommandAction(cmd, false) + } + + case *ast.ValueSpec: + // package-level var initializer? + if len(node.Names) > 0 && len(node.Values) > 0 { + v := req.pkg.TypesInfo().Defs[node.Names[0]] + if v != nil && typesinternal.IsPackageLevel(v) { + sym.WriteString("init") + name = "package initializer" // success } } } + + if name != "" { + cmd := command.NewAssemblyCommand( + fmt.Sprintf("Browse %s assembly for %s", view.GOARCH(), name), + view.ID(), + string(req.pkg.Metadata().ID), + sym.String()) + req.addCommandAction(cmd, false) + break + } } return nil } diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go index 2bde7df8aa2..5153289941f 100644 --- a/gopls/internal/test/integration/misc/webserver_test.go +++ b/gopls/internal/test/integration/misc/webserver_test.go @@ -520,43 +520,57 @@ module example.com -- a/a.go -- package a -func f() { +func f(x int) int { println("hello") defer println("world") + return x } func g() { println("goodbye") } + +var v = [...]int{ + f(123), + f(456), +} + +func init() { + f(789) +} ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - // Invoke the "Browse assembly" code action to start the server. - loc := env.RegexpSearch("a/a.go", "println") - actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger) - if err != nil { - t.Fatalf("CodeAction: %v", err) - } - action, err := codeActionByKind(actions, settings.GoAssembly) - if err != nil { - t.Fatal(err) - } + asmFor := func(pattern string) []byte { + // Invoke the "Browse assembly" code action to start the server. + loc := env.RegexpSearch("a/a.go", pattern) + actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger) + if err != nil { + t.Fatalf("CodeAction: %v", err) + } + action, err := codeActionByKind(actions, settings.GoAssembly) + if err != nil { + t.Fatal(err) + } - // Execute the command. - // Its side effect should be a single showDocument request. - params := &protocol.ExecuteCommandParams{ - Command: action.Command.Command, - Arguments: action.Command.Arguments, - } - var result command.DebuggingResult - collectDocs := env.Awaiter.ListenToShownDocuments() - env.ExecuteCommand(params, &result) - doc := shownDocument(t, collectDocs(), "http:") - if doc == nil { - t.Fatalf("no showDocument call had 'file:' prefix") + // Execute the command. + // Its side effect should be a single showDocument request. + params := &protocol.ExecuteCommandParams{ + Command: action.Command.Command, + Arguments: action.Command.Arguments, + } + var result command.DebuggingResult + collectDocs := env.Awaiter.ListenToShownDocuments() + env.ExecuteCommand(params, &result) + doc := shownDocument(t, collectDocs(), "http:") + if doc == nil { + t.Fatalf("no showDocument call had 'file:' prefix") + } + t.Log("showDocument(package doc) URL:", doc.URI) + + return get(t, doc.URI) } - t.Log("showDocument(package doc) URL:", doc.URI) // Get the report and do some minimal checks for sensible results. // @@ -567,23 +581,42 @@ func g() { // (e.g. uses JAL for CALL, or BL for RET). // We conservatively test only on the two most popular // architectures. - report := get(t, doc.URI) - checkMatch(t, true, report, `TEXT.*example.com/a.f`) - switch runtime.GOARCH { - case "amd64", "arm64": - checkMatch(t, true, report, `CALL runtime.printlock`) - checkMatch(t, true, report, `CALL runtime.printstring`) - checkMatch(t, true, report, `CALL runtime.printunlock`) - checkMatch(t, true, report, `CALL example.com/a.f.deferwrap1`) - checkMatch(t, true, report, `RET`) - checkMatch(t, true, report, `CALL runtime.morestack_noctxt`) + { + report := asmFor("println") + checkMatch(t, true, report, `TEXT.*example.com/a.f`) + switch runtime.GOARCH { + case "amd64", "arm64": + checkMatch(t, true, report, `CALL runtime.printlock`) + checkMatch(t, true, report, `CALL runtime.printstring`) + checkMatch(t, true, report, `CALL runtime.printunlock`) + checkMatch(t, true, report, `CALL example.com/a.f.deferwrap1`) + checkMatch(t, true, report, `RET`) + checkMatch(t, true, report, `CALL runtime.morestack_noctxt`) + } + + // Nested functions are also shown. + checkMatch(t, true, report, `TEXT.*example.com/a.f.deferwrap1`) + + // But other functions are not. + checkMatch(t, false, report, `TEXT.*example.com/a.g`) } - // Nested functions are also shown. - checkMatch(t, true, report, `TEXT.*example.com/a.f.deferwrap1`) + // Check that code in a package-level var initializer is found too. + { + report := asmFor(`f\(123\)`) + checkMatch(t, true, report, `TEXT.*example.com/a.init`) + checkMatch(t, true, report, `MOV. \$123`) + checkMatch(t, true, report, `MOV. \$456`) + checkMatch(t, true, report, `CALL example.com/a.f`) + } - // But other functions are not. - checkMatch(t, false, report, `TEXT.*example.com/a.g`) + // And code in a source-level init function. + { + report := asmFor(`f\(789\)`) + checkMatch(t, true, report, `TEXT.*example.com/a.init`) + checkMatch(t, true, report, `MOV. \$789`) + checkMatch(t, true, report, `CALL example.com/a.f`) + } }) } From 6f7906b2b92af49e85ca2e34f08a7097a19b6b5a Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 13 Feb 2025 13:21:46 -0500 Subject: [PATCH 70/99] x/tools: use ast.IsGenerated throughout Note the behavior change: the go/ast implementation checks that the special comment appears before the packge declaration; the ad hoc implementations did not. Change-Id: Ib51c498c1c73fd32c882e25f8228a6076bba7ed7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649316 LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Reviewed-by: Jonathan Amsterdam Commit-Queue: Alan Donovan --- cmd/deadcode/deadcode.go | 41 +------------------ copyright/copyright.go | 23 +---------- gopls/internal/golang/format.go | 11 ++--- gopls/internal/golang/util.go | 26 ++---------- .../diagnostics/diagnostics_test.go | 17 +++++--- .../marker/testdata/diagnostics/generated.txt | 4 +- internal/refactor/inline/inline.go | 4 +- refactor/rename/rename.go | 2 +- refactor/rename/spec.go | 25 +---------- 9 files changed, 29 insertions(+), 124 deletions(-) diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go index f129102cc4c..0c66d07f79f 100644 --- a/cmd/deadcode/deadcode.go +++ b/cmd/deadcode/deadcode.go @@ -175,7 +175,7 @@ func main() { } } - if isGenerated(file) { + if ast.IsGenerated(file) { generated[p.Fset.File(file.Pos()).Name()] = true } } @@ -414,45 +414,6 @@ func printObjects(format string, objects []any) { } } -// TODO(adonovan): use go1.21's ast.IsGenerated. - -// isGenerated reports whether the file was generated by a program, -// not handwritten, by detecting the special comment described -// at https://go.dev/s/generatedcode. -// -// The syntax tree must have been parsed with the ParseComments flag. -// Example: -// -// f, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.PackageClauseOnly) -// if err != nil { ... } -// gen := ast.IsGenerated(f) -func isGenerated(file *ast.File) bool { - _, ok := generator(file) - return ok -} - -func generator(file *ast.File) (string, bool) { - for _, group := range file.Comments { - for _, comment := range group.List { - if comment.Pos() > file.Package { - break // after package declaration - } - // opt: check Contains first to avoid unnecessary array allocation in Split. - const prefix = "// Code generated " - if strings.Contains(comment.Text, prefix) { - for _, line := range strings.Split(comment.Text, "\n") { - if rest, ok := strings.CutPrefix(line, prefix); ok { - if gen, ok := strings.CutSuffix(rest, " DO NOT EDIT."); ok { - return gen, true - } - } - } - } - } - } - return "", false -} - // pathSearch returns the shortest path from one of the roots to one // of the targets (along with the root itself), or zero if no path was found. func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Function]bool) (*callgraph.Node, []*callgraph.Edge) { diff --git a/copyright/copyright.go b/copyright/copyright.go index 54bc8f512a4..4d4ad71fd72 100644 --- a/copyright/copyright.go +++ b/copyright/copyright.go @@ -75,7 +75,7 @@ func checkFile(toolsDir, filename string) (bool, error) { return false, err } // Don't require headers on generated files. - if isGenerated(fset, parsed) { + if ast.IsGenerated(parsed) { return false, nil } shouldAddCopyright := true @@ -91,24 +91,3 @@ func checkFile(toolsDir, filename string) (bool, error) { } return shouldAddCopyright, nil } - -// Copied from golang.org/x/tools/gopls/internal/golang/util.go. -// Matches cgo generated comment as well as the proposed standard: -// -// https://golang.org/s/generatedcode -var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`) - -func isGenerated(fset *token.FileSet, file *ast.File) bool { - for _, commentGroup := range file.Comments { - for _, comment := range commentGroup.List { - if matched := generatedRx.MatchString(comment.Text); !matched { - continue - } - // Check if comment is at the beginning of the line in source. - if pos := fset.Position(comment.Slash); pos.Column == 1 { - return true - } - } - } - return false -} diff --git a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go index b353538d978..ded00deef38 100644 --- a/gopls/internal/golang/format.go +++ b/gopls/internal/golang/format.go @@ -35,15 +35,16 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr ctx, done := event.Start(ctx, "golang.Format") defer done() - // Generated files shouldn't be edited. So, don't format them - if IsGenerated(ctx, snapshot, fh.URI()) { - return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Path()) - } - pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } + + // Generated files shouldn't be edited. So, don't format them. + if ast.IsGenerated(pgf.File) { + return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Path()) + } + // Even if this file has parse errors, it might still be possible to format it. // Using format.Node on an AST with errors may result in code being modified. // Attempt to format the source of this file instead. diff --git a/gopls/internal/golang/util.go b/gopls/internal/golang/util.go index 23fd3443fac..a81ff3fbe58 100644 --- a/gopls/internal/golang/util.go +++ b/gopls/internal/golang/util.go @@ -19,16 +19,11 @@ 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/gopls/internal/util/safetoken" "golang.org/x/tools/internal/tokeninternal" ) -// IsGenerated gets and reads the file denoted by uri and reports -// whether it contains a "generated file" comment as described at -// https://golang.org/s/generatedcode. -// -// TODO(adonovan): opt: this function does too much. -// Move snapshot.ReadFile into the caller (most of which have already done it). +// IsGenerated reads and parses the header of the file denoted by uri +// and reports whether it [ast.IsGenerated]. func IsGenerated(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) bool { fh, err := snapshot.ReadFile(ctx, uri) if err != nil { @@ -38,17 +33,7 @@ func IsGenerated(ctx context.Context, snapshot *cache.Snapshot, uri protocol.Doc if err != nil { return false } - for _, commentGroup := range pgf.File.Comments { - for _, comment := range commentGroup.List { - if matched := generatedRx.MatchString(comment.Text); matched { - // Check if comment is at the beginning of the line in source. - if safetoken.Position(pgf.Tok, comment.Slash).Column == 1 { - return true - } - } - } - } - return false + return ast.IsGenerated(pgf.File) } // adjustedObjEnd returns the end position of obj, possibly modified for @@ -76,11 +61,6 @@ func adjustedObjEnd(obj types.Object) token.Pos { return obj.Pos() + token.Pos(nameLen) } -// Matches cgo generated comment as well as the proposed standard: -// -// https://golang.org/s/generatedcode -var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`) - // FormatNode returns the "pretty-print" output for an ast node. func FormatNode(fset *token.FileSet, n ast.Node) string { var buf strings.Builder diff --git a/gopls/internal/test/integration/diagnostics/diagnostics_test.go b/gopls/internal/test/integration/diagnostics/diagnostics_test.go index a97d249e7b5..5ef39a5f0c5 100644 --- a/gopls/internal/test/integration/diagnostics/diagnostics_test.go +++ b/gopls/internal/test/integration/diagnostics/diagnostics_test.go @@ -542,27 +542,34 @@ var X = 0 // Tests golang/go#38467. func TestNoSuggestedFixesForGeneratedFiles_Issue38467(t *testing.T) { + // This test ensures that gopls' CodeAction handler suppresses + // diagnostics in generated code. Beware that many analyzers + // themselves suppress diagnostics in generated files, in + // particular the low-status "simplifiers" (modernize, + // simplify{range,slice,compositelit}), so we use the hostport + // analyzer here. const generated = ` -- go.mod -- module mod.com go 1.12 -- main.go -- +// Code generated by generator.go. DO NOT EDIT. + package main -// Code generated by generator.go. DO NOT EDIT. +import ("fmt"; "net") func _() { - for i, _ := range []string{} { - _ = i - } + addr := fmt.Sprintf("%s:%d", "localhost", 12345) + net.Dial("tcp", addr) } ` Run(t, generated, func(t *testing.T, env *Env) { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - Diagnostics(AtPosition("main.go", 5, 6)), + Diagnostics(AtPosition("main.go", 7, 21)), ReadDiagnostics("main.go", &d), ) if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { diff --git a/gopls/internal/test/marker/testdata/diagnostics/generated.txt b/gopls/internal/test/marker/testdata/diagnostics/generated.txt index 123602df3c3..80de61200a3 100644 --- a/gopls/internal/test/marker/testdata/diagnostics/generated.txt +++ b/gopls/internal/test/marker/testdata/diagnostics/generated.txt @@ -10,10 +10,10 @@ module example.com go 1.12 -- generated.go -- -package generated - // Code generated by generator.go. DO NOT EDIT. +package generated + func _() { var y int //@diag("y", re"declared (and|but) not used") } diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 54308243e1c..6f6ed4583a9 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -102,9 +102,9 @@ func (st *state) inline() (*Result, error) { 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?)") } - // TODO(adonovan): use go1.21's ast.IsGenerated. // Break the string literal so we can use inlining in this file. :) - if bytes.Contains(caller.Content, []byte("// Code generated by "+"cmd/cgo; DO NOT EDIT.")) { + 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\"") } diff --git a/refactor/rename/rename.go b/refactor/rename/rename.go index 3e944b2df38..cb218434e49 100644 --- a/refactor/rename/rename.go +++ b/refactor/rename/rename.go @@ -491,7 +491,7 @@ func (r *renamer) update() error { for _, info := range r.packages { for _, f := range info.Files { tokenFile := r.iprog.Fset.File(f.FileStart) - if filesToUpdate[tokenFile] && generated(f, tokenFile) { + if filesToUpdate[tokenFile] && ast.IsGenerated(f) { generatedFileNames = append(generatedFileNames, tokenFile.Name()) } } diff --git a/refactor/rename/spec.go b/refactor/rename/spec.go index 99068c13358..0a6d7d4346c 100644 --- a/refactor/rename/spec.go +++ b/refactor/rename/spec.go @@ -19,7 +19,6 @@ import ( "log" "os" "path/filepath" - "regexp" "strconv" "strings" @@ -321,7 +320,7 @@ func findFromObjectsInFile(iprog *loader.Program, spec *spec) ([]types.Object, e if spec.offset != 0 { // We cannot refactor generated files since position information is invalidated. - if generated(f, thisFile) { + if ast.IsGenerated(f) { return nil, fmt.Errorf("cannot rename identifiers in generated file containing DO NOT EDIT marker: %s", thisFile.Name()) } @@ -566,25 +565,3 @@ func ambiguityError(fset *token.FileSet, objects []types.Object) error { return fmt.Errorf("ambiguous specifier %s matches %s", objects[0].Name(), buf.String()) } - -// Matches cgo generated comment as well as the proposed standard: -// -// https://golang.org/s/generatedcode -var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`) - -// generated reports whether ast.File is a generated file. -func generated(f *ast.File, tokenFile *token.File) bool { - - // Iterate over the comments in the file - for _, commentGroup := range f.Comments { - for _, comment := range commentGroup.List { - if matched := generatedRx.MatchString(comment.Text); matched { - // Check if comment is at the beginning of the line in source - if pos := tokenFile.Position(comment.Slash); pos.Column == 1 { - return true - } - } - } - } - return false -} From 7fed2a4a04b822b897c3dd789a11e027c9ad1b0c Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 25 Feb 2025 16:02:44 -0500 Subject: [PATCH 71/99] gopls/internal/analysis/modernize: fix bug in rangeint for and range loops leave their index variables with a different final value: limit, and limit-1, respectively. Thus we must not offer a fix if the loop variable is used after the loop. + test Fixes golang/go#71952 Change-Id: Iaabd20792724166ace0ed5fd9dd997edaa96a435 Reviewed-on: https://go-review.googlesource.com/c/tools/+/652496 Auto-Submit: Alan Donovan Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/modernize/rangeint.go | 12 ++++++++++++ .../modernize/testdata/src/rangeint/rangeint.go | 17 ++++++++++++++++- .../testdata/src/rangeint/rangeint.go.golden | 17 ++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/gopls/internal/analysis/modernize/rangeint.go b/gopls/internal/analysis/modernize/rangeint.go index 273c13877bd..b94bff34431 100644 --- a/gopls/internal/analysis/modernize/rangeint.go +++ b/gopls/internal/analysis/modernize/rangeint.go @@ -100,6 +100,18 @@ func rangeint(pass *analysis.Pass) { }) } + // If i is used after the loop, + // 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)) { + id := curId.Node().(*ast.Ident) + if id.Pos() > loop.End() && info.Uses[id] == v { + continue nextLoop + } + } + } + // If limit is len(slice), // simplify "range len(slice)" to "range slice". if call, ok := limit.(*ast.CallExpr); ok && diff --git a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go index 6c30f183340..32628f5fae3 100644 --- a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go +++ b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go @@ -4,7 +4,11 @@ func _(i int, s struct{ i int }, slice []int) { for i := 0; i < 10; i++ { // want "for loop can be modernized using range over int" println(i) } - for i = 0; i < f(); i++ { // want "for loop can be modernized using range over int" + { + var i int + for i = 0; i < f(); i++ { // want "for loop can be modernized using range over int" + } + // NB: no uses of i after loop. } for i := 0; i < 10; i++ { // want "for loop can be modernized using range over int" // i unused within loop @@ -51,3 +55,14 @@ func _(s string) { } } } + +// Repro for #71952: for and range loops have different final values +// on i (n and n-1, respectively) so we can't offer the fix if i is +// used after the loop. +func nopePostconditionDiffers() { + i := 0 + for i = 0; i < 5; i++ { + println(i) + } + println(i) // must print 5, not 4 +} diff --git a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden index 52f16347b1e..43cf220d699 100644 --- a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden @@ -4,7 +4,11 @@ func _(i int, s struct{ i int }, slice []int) { for i := range 10 { // want "for loop can be modernized using range over int" println(i) } - for i = range f() { // want "for loop can be modernized using range over int" + { + var i int + for i = range f() { // want "for loop can be modernized using range over int" + } + // NB: no uses of i after loop. } for range 10 { // want "for loop can be modernized using range over int" // i unused within loop @@ -51,3 +55,14 @@ func _(s string) { } } } + +// Repro for #71952: for and range loops have different final values +// on i (n and n-1, respectively) so we can't offer the fix if i is +// used after the loop. +func nopePostconditionDiffers() { + i := 0 + for i = 0; i < 5; i++ { + println(i) + } + println(i) // must print 5, not 4 +} From 6399d21203019d71e290a17b26c0946f3152cba0 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 14 Feb 2025 09:42:49 -0500 Subject: [PATCH 72/99] go/analysis/passes/reflectvaluecompare/cmd/reflectvaluecompare: add main.go This makes it easier to play with. Updates golang/go#71732 Change-Id: If5ec810c051b0c12ec30891c9a431cc5ca06dcd9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649615 Reviewed-by: Keith Randall Reviewed-by: Keith Randall Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- .../cmd/reflectvaluecompare/main.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 go/analysis/passes/reflectvaluecompare/cmd/reflectvaluecompare/main.go diff --git a/go/analysis/passes/reflectvaluecompare/cmd/reflectvaluecompare/main.go b/go/analysis/passes/reflectvaluecompare/cmd/reflectvaluecompare/main.go new file mode 100644 index 00000000000..f3f9e163913 --- /dev/null +++ b/go/analysis/passes/reflectvaluecompare/cmd/reflectvaluecompare/main.go @@ -0,0 +1,18 @@ +// 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. + +// The reflectvaluecompare command applies the reflectvaluecompare +// checker to the specified packages of Go source code. +// +// Run with: +// +// $ go run ./go/analysis/passes/reflectvaluecompare/cmd/reflectvaluecompare -- packages... +package main + +import ( + "golang.org/x/tools/go/analysis/passes/reflectvaluecompare" + "golang.org/x/tools/go/analysis/singlechecker" +) + +func main() { singlechecker.Main(reflectvaluecompare.Analyzer) } From 5dc980c6debffbe1b319cf554f28eaf100b9fc94 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 25 Feb 2025 22:24:09 -0500 Subject: [PATCH 73/99] gopls/internal/test/integration/misc: fix "want" assembly MOVD, MOVL, MOV are all valid. The latter appears in riscv. Fixes golang/go#71956 Change-Id: I74aa3d9a47a20b44d398054e7184e984c6701ca0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/652359 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- gopls/internal/test/integration/misc/webserver_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go index 5153289941f..4b495dfa07e 100644 --- a/gopls/internal/test/integration/misc/webserver_test.go +++ b/gopls/internal/test/integration/misc/webserver_test.go @@ -605,8 +605,8 @@ func init() { { report := asmFor(`f\(123\)`) checkMatch(t, true, report, `TEXT.*example.com/a.init`) - checkMatch(t, true, report, `MOV. \$123`) - checkMatch(t, true, report, `MOV. \$456`) + checkMatch(t, true, report, `MOV.? \$123`) + checkMatch(t, true, report, `MOV.? \$456`) checkMatch(t, true, report, `CALL example.com/a.f`) } @@ -614,7 +614,7 @@ func init() { { report := asmFor(`f\(789\)`) checkMatch(t, true, report, `TEXT.*example.com/a.init`) - checkMatch(t, true, report, `MOV. \$789`) + checkMatch(t, true, report, `MOV.? \$789`) checkMatch(t, true, report, `CALL example.com/a.f`) } }) From 779331ac58c17baf109674a5754c0f0c630f695a Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 26 Feb 2025 07:50:27 -0500 Subject: [PATCH 74/99] gopls/internal/test/integration/misc: only test asm on {arm,amd}64 Fixes golang/go#71956 for real this time Change-Id: I3db07168da163ea6c8fdeefda28f64b94fe2ed57 Reviewed-on: https://go-review.googlesource.com/c/tools/+/652695 Reviewed-by: Robert Findley Commit-Queue: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- .../test/integration/misc/webserver_test.go | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go index 4b495dfa07e..79a6548ee3e 100644 --- a/gopls/internal/test/integration/misc/webserver_test.go +++ b/gopls/internal/test/integration/misc/webserver_test.go @@ -604,18 +604,24 @@ func init() { // Check that code in a package-level var initializer is found too. { report := asmFor(`f\(123\)`) - checkMatch(t, true, report, `TEXT.*example.com/a.init`) - checkMatch(t, true, report, `MOV.? \$123`) - checkMatch(t, true, report, `MOV.? \$456`) - checkMatch(t, true, report, `CALL example.com/a.f`) + switch runtime.GOARCH { + case "amd64", "arm64": + checkMatch(t, true, report, `TEXT.*example.com/a.init`) + checkMatch(t, true, report, `MOV.? \$123`) + checkMatch(t, true, report, `MOV.? \$456`) + checkMatch(t, true, report, `CALL example.com/a.f`) + } } // And code in a source-level init function. { report := asmFor(`f\(789\)`) - checkMatch(t, true, report, `TEXT.*example.com/a.init`) - checkMatch(t, true, report, `MOV.? \$789`) - checkMatch(t, true, report, `CALL example.com/a.f`) + switch runtime.GOARCH { + case "amd64", "arm64": + checkMatch(t, true, report, `TEXT.*example.com/a.init`) + checkMatch(t, true, report, `MOV.? \$789`) + checkMatch(t, true, report, `CALL example.com/a.f`) + } } }) } From d740adf9c34bc9f6c7944b62fd3fd15851ed8fc0 Mon Sep 17 00:00:00 2001 From: danztran Date: Wed, 26 Feb 2025 06:18:57 +0000 Subject: [PATCH 75/99] gopls/internal/settings: correct SemanticTokenTypes source fix golang/go#71964 Change-Id: I2694023636272ea971880865a4f2cb6d9192d7d5 GitHub-Last-Rev: c81d388cfe13667504cff275c969fc81587c6fc9 GitHub-Pull-Request: golang/tools#564 Reviewed-on: https://go-review.googlesource.com/c/tools/+/652655 Reviewed-by: Hongxiang Jiang LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Auto-Submit: Hongxiang Jiang --- gopls/internal/settings/settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index dd353da64e9..11b06040181 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -1356,7 +1356,7 @@ func (o *Options) EnabledSemanticTokenModifiers() map[semtok.Modifier]bool { // EncodeSemanticTokenTypes returns a map of types to boolean. func (o *Options) EnabledSemanticTokenTypes() map[semtok.Type]bool { copy := make(map[semtok.Type]bool, len(o.SemanticTokenTypes)) - for k, v := range o.SemanticTokenModifiers { + for k, v := range o.SemanticTokenTypes { copy[semtok.Type(k)] = v } if o.NoSemanticString { From 63229bc79404d8cf2fe4e88ad569168fe251d993 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 26 Feb 2025 14:20:40 -0500 Subject: [PATCH 76/99] gopls/internal/analysis/gofix: register "alias" fact type Fixes golang/go#71982 Change-Id: I29535d430e2fb9da0915a1d6ec99d4a3ade8e4e8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/652975 Auto-Submit: Alan Donovan Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/gofix/gofix.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index 237e5b0b58a..7323028aa31 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -30,12 +30,16 @@ import ( var doc string var Analyzer = &analysis.Analyzer{ - Name: "gofix", - Doc: analysisinternal.MustExtractDoc(doc, "gofix"), - URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/gofix", - Run: run, - FactTypes: []analysis.Fact{new(goFixInlineFuncFact), new(goFixInlineConstFact)}, - Requires: []*analysis.Analyzer{inspect.Analyzer}, + Name: "gofix", + Doc: analysisinternal.MustExtractDoc(doc, "gofix"), + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/gofix", + Run: run, + FactTypes: []analysis.Fact{ + (*goFixInlineFuncFact)(nil), + (*goFixInlineConstFact)(nil), + (*goFixInlineAliasFact)(nil), + }, + Requires: []*analysis.Analyzer{inspect.Analyzer}, } // analyzer holds the state for this analysis. From 57b529ad205da65cbc7429c2cadd5d7c44055981 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Tue, 25 Feb 2025 11:10:41 -0500 Subject: [PATCH 77/99] doc/release/v0.18.0.md: add -fix flag Added the -fix flag to the command line for applying go:fix fixes. The given command prints the fixes, but does not apply them. Change-Id: Ia6dc100cf88e293453fbc6649f14aa0046572104 Reviewed-on: https://go-review.googlesource.com/c/tools/+/652355 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/doc/release/v0.18.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/doc/release/v0.18.0.md b/gopls/doc/release/v0.18.0.md index ba2c0184307..9aa0f9c9d07 100644 --- a/gopls/doc/release/v0.18.0.md +++ b/gopls/doc/release/v0.18.0.md @@ -106,7 +106,7 @@ gopls will suggest replacing `Ptr` in your code with `Pointer`. Use this command to apply such fixes en masse: ``` -$ go run golang.org/x/tools/gopls/internal/analysis/gofix/cmd/gofix@latest -test ./... +$ go run golang.org/x/tools/gopls/internal/analysis/gofix/cmd/gofix@latest -test -fix ./... ``` ## "Implementations" supports generics From 8f4b8cd6b69a761defc548aa8377b8306a881c20 Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Thu, 27 Feb 2025 11:28:35 -0500 Subject: [PATCH 78/99] gopls/internal/golang: add package symbols comment Note and explain why we use only syntax and not type information to parse package symbols. Change-Id: I7498158cf633e82d4149f88fc7e8858babd66559 Reviewed-on: https://go-review.googlesource.com/c/tools/+/653355 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/golang/symbols.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gopls/internal/golang/symbols.go b/gopls/internal/golang/symbols.go index db31baa69f2..53fbb663800 100644 --- a/gopls/internal/golang/symbols.go +++ b/gopls/internal/golang/symbols.go @@ -82,6 +82,9 @@ func DocumentSymbols(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand // The PackageSymbol data type contains the same fields as protocol.DocumentSymbol, with // an additional int field "File" that stores the index of that symbol's file in the // PackageSymbolsResult.Files. +// Symbols are gathered using syntax rather than type information because type checking is +// significantly slower. Syntax information provides enough value to the user without +// causing a lag when loading symbol information across different files. func PackageSymbols(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (command.PackageSymbolsResult, error) { ctx, done := event.Start(ctx, "source.PackageSymbols") defer done() From 1cc80ad525837f752d516a5827e78bce18755cd2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 26 Feb 2025 13:35:43 -0500 Subject: [PATCH 79/99] internal/event/export/ocagent: delete We never use it, and OpenCensus is officially moribund. Change-Id: I0095f996c58954c238be7875694ee62dd721b3f2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/653016 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Commit-Queue: Alan Donovan Auto-Submit: Alan Donovan --- gopls/internal/cmd/cmd.go | 7 +- gopls/internal/cmd/usage/usage-v.hlp | 2 - gopls/internal/cmd/usage/usage.hlp | 2 - gopls/internal/debug/serve.go | 15 +- gopls/internal/lsprpc/lsprpc_test.go | 8 +- gopls/internal/test/integration/runner.go | 4 +- gopls/internal/test/marker/marker_test.go | 2 +- internal/event/export/ocagent/README.md | 139 ------- internal/event/export/ocagent/metrics.go | 213 ----------- internal/event/export/ocagent/metrics_test.go | 144 ------- internal/event/export/ocagent/ocagent.go | 358 ------------------ internal/event/export/ocagent/ocagent_test.go | 210 ---------- internal/event/export/ocagent/trace_test.go | 158 -------- internal/event/export/ocagent/wire/common.go | 101 ----- internal/event/export/ocagent/wire/core.go | 17 - internal/event/export/ocagent/wire/metrics.go | 204 ---------- .../event/export/ocagent/wire/metrics_test.go | 80 ---- internal/event/export/ocagent/wire/trace.go | 112 ------ 18 files changed, 10 insertions(+), 1766 deletions(-) delete mode 100644 internal/event/export/ocagent/README.md delete mode 100644 internal/event/export/ocagent/metrics.go delete mode 100644 internal/event/export/ocagent/metrics_test.go delete mode 100644 internal/event/export/ocagent/ocagent.go delete mode 100644 internal/event/export/ocagent/ocagent_test.go delete mode 100644 internal/event/export/ocagent/trace_test.go delete mode 100644 internal/event/export/ocagent/wire/common.go delete mode 100644 internal/event/export/ocagent/wire/core.go delete mode 100644 internal/event/export/ocagent/wire/metrics.go delete mode 100644 internal/event/export/ocagent/wire/metrics_test.go delete mode 100644 internal/event/export/ocagent/wire/trace.go diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go index 119577c012b..4a00afc4115 100644 --- a/gopls/internal/cmd/cmd.go +++ b/gopls/internal/cmd/cmd.go @@ -63,9 +63,6 @@ type Application struct { // VeryVerbose enables a higher level of verbosity in logging output. VeryVerbose bool `flag:"vv,veryverbose" help:"very verbose output"` - // Control ocagent export of telemetry - OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), or off"` - // PrepareOptions is called to update the options when a new view is built. // It is primarily to allow the behavior of gopls to be modified by hooks. PrepareOptions func(*settings.Options) @@ -98,8 +95,6 @@ func (app *Application) verbose() bool { // New returns a new Application ready to run. func New() *Application { app := &Application{ - OCAgent: "off", //TODO: Remove this line to default the exporter to on - Serve: Serve{ RemoteListenTimeout: 1 * time.Minute, }, @@ -238,7 +233,7 @@ func (app *Application) Run(ctx context.Context, args ...string) error { // executable, and immediately runs a gc. filecache.Start() - ctx = debug.WithInstance(ctx, app.OCAgent) + ctx = debug.WithInstance(ctx) if len(args) == 0 { s := flag.NewFlagSet(app.Name(), flag.ExitOnError) return tool.Run(ctx, s, &app.Serve, args) diff --git a/gopls/internal/cmd/usage/usage-v.hlp b/gopls/internal/cmd/usage/usage-v.hlp index 64f99a3387e..044d4251e89 100644 --- a/gopls/internal/cmd/usage/usage-v.hlp +++ b/gopls/internal/cmd/usage/usage-v.hlp @@ -61,8 +61,6 @@ flags: filename to log to. if value is "auto", then logging to a default output file is enabled -mode=string no effect - -ocagent=string - the address of the ocagent (e.g. http://localhost:55678), or off (default "off") -port=int port on which to run gopls for debugging purposes -profile.alloc=string diff --git a/gopls/internal/cmd/usage/usage.hlp b/gopls/internal/cmd/usage/usage.hlp index c801a467626..b918b24a411 100644 --- a/gopls/internal/cmd/usage/usage.hlp +++ b/gopls/internal/cmd/usage/usage.hlp @@ -58,8 +58,6 @@ flags: filename to log to. if value is "auto", then logging to a default output file is enabled -mode=string no effect - -ocagent=string - the address of the ocagent (e.g. http://localhost:55678), or off (default "off") -port=int port on which to run gopls for debugging purposes -profile.alloc=string diff --git a/gopls/internal/debug/serve.go b/gopls/internal/debug/serve.go index c471f488cd1..7cfe2b3d23e 100644 --- a/gopls/internal/debug/serve.go +++ b/gopls/internal/debug/serve.go @@ -33,7 +33,6 @@ import ( "golang.org/x/tools/internal/event/core" "golang.org/x/tools/internal/event/export" "golang.org/x/tools/internal/event/export/metric" - "golang.org/x/tools/internal/event/export/ocagent" "golang.org/x/tools/internal/event/export/prometheus" "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/event/label" @@ -51,13 +50,11 @@ type Instance struct { Logfile string StartTime time.Time ServerAddress string - OCAgentConfig string LogWriter io.Writer exporter event.Exporter - ocagent *ocagent.Exporter prometheus *prometheus.Exporter rpcs *Rpcs traces *traces @@ -363,16 +360,11 @@ func GetInstance(ctx context.Context) *Instance { // WithInstance creates debug instance ready for use using the supplied // configuration and stores it in the returned context. -func WithInstance(ctx context.Context, agent string) context.Context { +func WithInstance(ctx context.Context) context.Context { i := &Instance{ - StartTime: time.Now(), - OCAgentConfig: agent, + StartTime: time.Now(), } i.LogWriter = os.Stderr - ocConfig := ocagent.Discover() - //TODO: we should not need to adjust the discovered configuration - ocConfig.Address = i.OCAgentConfig - i.ocagent = ocagent.Connect(ocConfig) i.prometheus = prometheus.New() i.rpcs = &Rpcs{} i.traces = &traces{} @@ -541,9 +533,6 @@ func messageType(l log.Level) protocol.MessageType { func makeInstanceExporter(i *Instance) event.Exporter { exporter := func(ctx context.Context, ev core.Event, lm label.Map) context.Context { - if i.ocagent != nil { - ctx = i.ocagent.ProcessEvent(ctx, ev, lm) - } if i.prometheus != nil { ctx = i.prometheus.ProcessEvent(ctx, ev, lm) } diff --git a/gopls/internal/lsprpc/lsprpc_test.go b/gopls/internal/lsprpc/lsprpc_test.go index 1a259bbd646..eda00b28c7a 100644 --- a/gopls/internal/lsprpc/lsprpc_test.go +++ b/gopls/internal/lsprpc/lsprpc_test.go @@ -58,7 +58,7 @@ func TestClientLogging(t *testing.T) { server := PingServer{} client := FakeClient{Logs: make(chan string, 10)} - ctx = debug.WithInstance(ctx, "") + ctx = debug.WithInstance(ctx) ss := NewStreamServer(cache.New(nil), false, nil).(*StreamServer) ss.serverForTest = server ts := servertest.NewPipeServer(ss, nil) @@ -121,7 +121,7 @@ func checkClose(t *testing.T, closer func() error) { func setupForwarding(ctx context.Context, t *testing.T, s protocol.Server) (direct, forwarded servertest.Connector, cleanup func()) { t.Helper() - serveCtx := debug.WithInstance(ctx, "") + serveCtx := debug.WithInstance(ctx) ss := NewStreamServer(cache.New(nil), false, nil).(*StreamServer) ss.serverForTest = s tsDirect := servertest.NewTCPServer(serveCtx, ss, nil) @@ -214,8 +214,8 @@ func TestDebugInfoLifecycle(t *testing.T) { baseCtx, cancel := context.WithCancel(context.Background()) defer cancel() - clientCtx := debug.WithInstance(baseCtx, "") - serverCtx := debug.WithInstance(baseCtx, "") + clientCtx := debug.WithInstance(baseCtx) + serverCtx := debug.WithInstance(baseCtx) cache := cache.New(nil) ss := NewStreamServer(cache, false, nil) diff --git a/gopls/internal/test/integration/runner.go b/gopls/internal/test/integration/runner.go index 6d10b16cab3..b3e98b859d3 100644 --- a/gopls/internal/test/integration/runner.go +++ b/gopls/internal/test/integration/runner.go @@ -173,7 +173,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio } // TODO(rfindley): do we need an instance at all? Can it be removed? - ctx = debug.WithInstance(ctx, "off") + ctx = debug.WithInstance(ctx) rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name())) if err := os.MkdirAll(rootDir, 0755); err != nil { @@ -349,7 +349,7 @@ func (r *Runner) defaultServer() jsonrpc2.StreamServer { func (r *Runner) forwardedServer() jsonrpc2.StreamServer { r.tsOnce.Do(func() { ctx := context.Background() - ctx = debug.WithInstance(ctx, "off") + ctx = debug.WithInstance(ctx) ss := lsprpc.NewStreamServer(cache.New(nil), false, nil) r.ts = servertest.NewTCPServer(ctx, ss, nil) }) diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index a5e23b928ad..a3e62d35968 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -964,7 +964,7 @@ func newEnv(t *testing.T, cache *cache.Cache, files, proxyFiles map[string][]byt // Put a debug instance in the context to prevent logging to stderr. // See associated TODO in runner.go: we should revisit this pattern. ctx := context.Background() - ctx = debug.WithInstance(ctx, "off") + ctx = debug.WithInstance(ctx) awaiter := integration.NewAwaiter(sandbox.Workdir) ss := lsprpc.NewStreamServer(cache, false, nil) diff --git a/internal/event/export/ocagent/README.md b/internal/event/export/ocagent/README.md deleted file mode 100644 index 22e8469f06b..00000000000 --- a/internal/event/export/ocagent/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# Exporting Metrics and Traces with OpenCensus, Zipkin, and Prometheus - -This tutorial provides a minimum example to verify that metrics and traces -can be exported to OpenCensus from Go tools. - -## Setting up oragent - -1. Ensure you have [docker](https://www.docker.com/get-started) and [docker-compose](https://docs.docker.com/compose/install/). -2. Clone [oragent](https://github.com/orijtech/oragent). -3. In the oragent directory, start the services: -```bash -docker-compose up -``` -If everything goes well, you should see output resembling the following: -``` -Starting oragent_zipkin_1 ... done -Starting oragent_oragent_1 ... done -Starting oragent_prometheus_1 ... done -... -``` -* You can check the status of the OpenCensus agent using zPages at http://localhost:55679/debug/tracez. -* You can now access the Prometheus UI at http://localhost:9445. -* You can now access the Zipkin UI at http://localhost:9444. -4. To shut down oragent, hit Ctrl+C in the terminal. -5. You can also start oragent in detached mode by running `docker-compose up -d`. To stop oragent while detached, run `docker-compose down`. - -## Exporting Metrics and Traces -1. Clone the [tools](https://golang.org/x/tools) subrepository. -1. Inside `internal`, create a file named `main.go` with the following contents: -```go -package main - -import ( - "context" - "fmt" - "math/rand" - "net/http" - "time" - - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/export" - "golang.org/x/tools/internal/event/export/metric" - "golang.org/x/tools/internal/event/export/ocagent" -) - -type testExporter struct { - metrics metric.Exporter - ocagent *ocagent.Exporter -} - -func (e *testExporter) ProcessEvent(ctx context.Context, ev event.Event) (context.Context, event.Event) { - ctx, ev = export.Tag(ctx, ev) - ctx, ev = export.ContextSpan(ctx, ev) - ctx, ev = e.metrics.ProcessEvent(ctx, ev) - ctx, ev = e.ocagent.ProcessEvent(ctx, ev) - return ctx, ev -} - -func main() { - exporter := &testExporter{} - - exporter.ocagent = ocagent.Connect(&ocagent.Config{ - Start: time.Now(), - Address: "http://127.0.0.1:55678", - Service: "go-tools-test", - Rate: 5 * time.Second, - Client: &http.Client{}, - }) - event.SetExporter(exporter) - - ctx := context.TODO() - mLatency := event.NewFloat64Key("latency", "the latency in milliseconds") - distribution := metric.HistogramFloat64Data{ - Info: &metric.HistogramFloat64{ - Name: "latencyDistribution", - Description: "the various latencies", - Buckets: []float64{0, 10, 50, 100, 200, 400, 800, 1000, 1400, 2000, 5000, 10000, 15000}, - }, - } - - distribution.Info.Record(&exporter.metrics, mLatency) - - for { - sleep := randomSleep() - _, end := event.StartSpan(ctx, "main.randomSleep()") - time.Sleep(time.Duration(sleep) * time.Millisecond) - end() - event.Record(ctx, mLatency.Of(float64(sleep))) - - fmt.Println("Latency: ", float64(sleep)) - } -} - -func randomSleep() int64 { - var max int64 - switch modulus := time.Now().Unix() % 5; modulus { - case 0: - max = 17001 - case 1: - max = 8007 - case 2: - max = 917 - case 3: - max = 87 - case 4: - max = 1173 - } - return rand.Int63n(max) -} - -``` -3. Run the new file from within the tools repository: -```bash -go run internal/main.go -``` -4. After about 5 seconds, OpenCensus should start receiving your new metrics, which you can see at http://localhost:8844/metrics. This page will look similar to the following: -``` -# HELP promdemo_latencyDistribution the various latencies -# TYPE promdemo_latencyDistribution histogram -promdemo_latencyDistribution_bucket{vendor="otc",le="0"} 0 -promdemo_latencyDistribution_bucket{vendor="otc",le="10"} 2 -promdemo_latencyDistribution_bucket{vendor="otc",le="50"} 9 -promdemo_latencyDistribution_bucket{vendor="otc",le="100"} 22 -promdemo_latencyDistribution_bucket{vendor="otc",le="200"} 35 -promdemo_latencyDistribution_bucket{vendor="otc",le="400"} 49 -promdemo_latencyDistribution_bucket{vendor="otc",le="800"} 63 -promdemo_latencyDistribution_bucket{vendor="otc",le="1000"} 78 -promdemo_latencyDistribution_bucket{vendor="otc",le="1400"} 93 -promdemo_latencyDistribution_bucket{vendor="otc",le="2000"} 108 -promdemo_latencyDistribution_bucket{vendor="otc",le="5000"} 123 -promdemo_latencyDistribution_bucket{vendor="otc",le="10000"} 138 -promdemo_latencyDistribution_bucket{vendor="otc",le="15000"} 153 -promdemo_latencyDistribution_bucket{vendor="otc",le="+Inf"} 15 -promdemo_latencyDistribution_sum{vendor="otc"} 1641 -promdemo_latencyDistribution_count{vendor="otc"} 15 -``` -5. After a few more seconds, Prometheus should start displaying your new metrics. You can view the distribution at http://localhost:9445/graph?g0.range_input=5m&g0.stacked=1&g0.expr=rate(oragent_latencyDistribution_bucket%5B5m%5D)&g0.tab=0. - -6. Zipkin should also start displaying traces. You can view them at http://localhost:9444/zipkin/?limit=10&lookback=300000&serviceName=go-tools-test. \ No newline at end of file diff --git a/internal/event/export/ocagent/metrics.go b/internal/event/export/ocagent/metrics.go deleted file mode 100644 index 78d65994db8..00000000000 --- a/internal/event/export/ocagent/metrics.go +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package ocagent - -import ( - "time" - - "golang.org/x/tools/internal/event/export/metric" - "golang.org/x/tools/internal/event/export/ocagent/wire" - "golang.org/x/tools/internal/event/label" -) - -// dataToMetricDescriptor return a *wire.MetricDescriptor based on data. -func dataToMetricDescriptor(data metric.Data) *wire.MetricDescriptor { - if data == nil { - return nil - } - descriptor := &wire.MetricDescriptor{ - Name: data.Handle(), - Description: getDescription(data), - // TODO: Unit? - Type: dataToMetricDescriptorType(data), - LabelKeys: getLabelKeys(data), - } - - return descriptor -} - -// getDescription returns the description of data. -func getDescription(data metric.Data) string { - switch d := data.(type) { - case *metric.Int64Data: - return d.Info.Description - - case *metric.Float64Data: - return d.Info.Description - - case *metric.HistogramInt64Data: - return d.Info.Description - - case *metric.HistogramFloat64Data: - return d.Info.Description - } - - return "" -} - -// getLabelKeys returns a slice of *wire.LabelKeys based on the keys -// in data. -func getLabelKeys(data metric.Data) []*wire.LabelKey { - switch d := data.(type) { - case *metric.Int64Data: - return infoKeysToLabelKeys(d.Info.Keys) - - case *metric.Float64Data: - return infoKeysToLabelKeys(d.Info.Keys) - - case *metric.HistogramInt64Data: - return infoKeysToLabelKeys(d.Info.Keys) - - case *metric.HistogramFloat64Data: - return infoKeysToLabelKeys(d.Info.Keys) - } - - return nil -} - -// dataToMetricDescriptorType returns a wire.MetricDescriptor_Type based on the -// underlying type of data. -func dataToMetricDescriptorType(data metric.Data) wire.MetricDescriptor_Type { - switch d := data.(type) { - case *metric.Int64Data: - if d.IsGauge { - return wire.MetricDescriptor_GAUGE_INT64 - } - return wire.MetricDescriptor_CUMULATIVE_INT64 - - case *metric.Float64Data: - if d.IsGauge { - return wire.MetricDescriptor_GAUGE_DOUBLE - } - return wire.MetricDescriptor_CUMULATIVE_DOUBLE - - case *metric.HistogramInt64Data: - return wire.MetricDescriptor_CUMULATIVE_DISTRIBUTION - - case *metric.HistogramFloat64Data: - return wire.MetricDescriptor_CUMULATIVE_DISTRIBUTION - } - - return wire.MetricDescriptor_UNSPECIFIED -} - -// dataToTimeseries returns a slice of *wire.TimeSeries based on the -// points in data. -func dataToTimeseries(data metric.Data, start time.Time) []*wire.TimeSeries { - if data == nil { - return nil - } - - numRows := numRows(data) - startTimestamp := convertTimestamp(start) - timeseries := make([]*wire.TimeSeries, 0, numRows) - - for i := 0; i < numRows; i++ { - timeseries = append(timeseries, &wire.TimeSeries{ - StartTimestamp: &startTimestamp, - // TODO: labels? - Points: dataToPoints(data, i), - }) - } - - return timeseries -} - -// numRows returns the number of rows in data. -func numRows(data metric.Data) int { - switch d := data.(type) { - case *metric.Int64Data: - return len(d.Rows) - case *metric.Float64Data: - return len(d.Rows) - case *metric.HistogramInt64Data: - return len(d.Rows) - case *metric.HistogramFloat64Data: - return len(d.Rows) - } - - return 0 -} - -// dataToPoints returns an array of *wire.Points based on the point(s) -// in data at index i. -func dataToPoints(data metric.Data, i int) []*wire.Point { - switch d := data.(type) { - case *metric.Int64Data: - timestamp := convertTimestamp(d.EndTime) - return []*wire.Point{ - { - Value: wire.PointInt64Value{ - Int64Value: d.Rows[i], - }, - Timestamp: ×tamp, - }, - } - case *metric.Float64Data: - timestamp := convertTimestamp(d.EndTime) - return []*wire.Point{ - { - Value: wire.PointDoubleValue{ - DoubleValue: d.Rows[i], - }, - Timestamp: ×tamp, - }, - } - case *metric.HistogramInt64Data: - row := d.Rows[i] - bucketBounds := make([]float64, len(d.Info.Buckets)) - for i, val := range d.Info.Buckets { - bucketBounds[i] = float64(val) - } - return distributionToPoints(row.Values, row.Count, float64(row.Sum), bucketBounds, d.EndTime) - case *metric.HistogramFloat64Data: - row := d.Rows[i] - return distributionToPoints(row.Values, row.Count, row.Sum, d.Info.Buckets, d.EndTime) - } - - return nil -} - -// distributionToPoints returns an array of *wire.Points containing a -// wire.PointDistributionValue representing a distribution with the -// supplied counts, count, and sum. -func distributionToPoints(counts []int64, count int64, sum float64, bucketBounds []float64, end time.Time) []*wire.Point { - buckets := make([]*wire.Bucket, len(counts)) - for i := 0; i < len(counts); i++ { - buckets[i] = &wire.Bucket{ - Count: counts[i], - } - } - timestamp := convertTimestamp(end) - return []*wire.Point{ - { - Value: wire.PointDistributionValue{ - DistributionValue: &wire.DistributionValue{ - Count: count, - Sum: sum, - // TODO: SumOfSquaredDeviation? - Buckets: buckets, - BucketOptions: &wire.BucketOptionsExplicit{ - Bounds: bucketBounds, - }, - }, - }, - Timestamp: ×tamp, - }, - } -} - -// infoKeysToLabelKeys returns an array of *wire.LabelKeys containing the -// string values of the elements of labelKeys. -func infoKeysToLabelKeys(infoKeys []label.Key) []*wire.LabelKey { - labelKeys := make([]*wire.LabelKey, 0, len(infoKeys)) - for _, key := range infoKeys { - labelKeys = append(labelKeys, &wire.LabelKey{ - Key: key.Name(), - }) - } - - return labelKeys -} diff --git a/internal/event/export/ocagent/metrics_test.go b/internal/event/export/ocagent/metrics_test.go deleted file mode 100644 index 001e7f02dbf..00000000000 --- a/internal/event/export/ocagent/metrics_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2020 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 ocagent_test - -import ( - "context" - "errors" - "testing" - - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/keys" -) - -func TestEncodeMetric(t *testing.T) { - exporter := registerExporter() - const prefix = testNodeStr + ` - "metrics":[` - const suffix = `]}` - tests := []struct { - name string - run func(ctx context.Context) - want string - }{ - { - name: "HistogramFloat64, HistogramInt64", - run: func(ctx context.Context) { - ctx = event.Label(ctx, keyMethod.Of("godoc.ServeHTTP")) - event.Metric(ctx, latencyMs.Of(96.58)) - ctx = event.Label(ctx, keys.Err.Of(errors.New("panic: fatal signal"))) - event.Metric(ctx, bytesIn.Of(97e2)) - }, - want: prefix + ` - { - "metric_descriptor": { - "name": "latency_ms", - "description": "The latency of calls in milliseconds", - "type": 6, - "label_keys": [ - { - "key": "method" - }, - { - "key": "route" - } - ] - }, - "timeseries": [ - { - "start_timestamp": "1970-01-01T00:00:00Z", - "points": [ - { - "timestamp": "1970-01-01T00:00:40Z", - "distributionValue": { - "count": 1, - "sum": 96.58, - "bucket_options": { - "explicit": { - "bounds": [ - 0, - 5, - 10, - 25, - 50 - ] - } - }, - "buckets": [ - {}, - {}, - {}, - {}, - {} - ] - } - } - ] - } - ] - }, - { - "metric_descriptor": { - "name": "latency_ms", - "description": "The latency of calls in milliseconds", - "type": 6, - "label_keys": [ - { - "key": "method" - }, - { - "key": "route" - } - ] - }, - "timeseries": [ - { - "start_timestamp": "1970-01-01T00:00:00Z", - "points": [ - { - "timestamp": "1970-01-01T00:00:40Z", - "distributionValue": { - "count": 1, - "sum": 9700, - "bucket_options": { - "explicit": { - "bounds": [ - 0, - 10, - 50, - 100, - 500, - 1000, - 2000 - ] - } - }, - "buckets": [ - {}, - {}, - {}, - {}, - {}, - {}, - {} - ] - } - } - ] - } - ] - }` + suffix, - }, - } - - ctx := context.TODO() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.run(ctx) - got := exporter.Output("/v1/metrics") - checkJSON(t, got, []byte(tt.want)) - }) - } -} diff --git a/internal/event/export/ocagent/ocagent.go b/internal/event/export/ocagent/ocagent.go deleted file mode 100644 index d86c4aed0cf..00000000000 --- a/internal/event/export/ocagent/ocagent.go +++ /dev/null @@ -1,358 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package ocagent adds the ability to export all telemetry to an ocagent. -// This keeps the compile time dependencies to zero and allows the agent to -// have the exporters needed for telemetry aggregation and viewing systems. -package ocagent - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "os" - "path/filepath" - "sync" - "time" - - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/core" - "golang.org/x/tools/internal/event/export" - "golang.org/x/tools/internal/event/export/metric" - "golang.org/x/tools/internal/event/export/ocagent/wire" - "golang.org/x/tools/internal/event/keys" - "golang.org/x/tools/internal/event/label" -) - -type Config struct { - Start time.Time - Host string - Process uint32 - Client *http.Client - Service string - Address string - Rate time.Duration -} - -var ( - connectMu sync.Mutex - exporters = make(map[Config]*Exporter) -) - -// Discover finds the local agent to export to, it will return nil if there -// is not one running. -// TODO: Actually implement a discovery protocol rather than a hard coded address -func Discover() *Config { - return &Config{ - Address: "http://localhost:55678", - } -} - -type Exporter struct { - mu sync.Mutex - config Config - spans []*export.Span - metrics []metric.Data -} - -// Connect creates a process specific exporter with the specified -// serviceName and the address of the ocagent to which it will upload -// its telemetry. -func Connect(config *Config) *Exporter { - if config == nil || config.Address == "off" { - return nil - } - resolved := *config - if resolved.Host == "" { - hostname, _ := os.Hostname() - resolved.Host = hostname - } - if resolved.Process == 0 { - resolved.Process = uint32(os.Getpid()) - } - if resolved.Client == nil { - resolved.Client = http.DefaultClient - } - if resolved.Service == "" { - resolved.Service = filepath.Base(os.Args[0]) - } - if resolved.Rate == 0 { - resolved.Rate = 2 * time.Second - } - - connectMu.Lock() - defer connectMu.Unlock() - if exporter, found := exporters[resolved]; found { - return exporter - } - exporter := &Exporter{config: resolved} - exporters[resolved] = exporter - if exporter.config.Start.IsZero() { - exporter.config.Start = time.Now() - } - go func() { - for range time.Tick(exporter.config.Rate) { - exporter.Flush() - } - }() - return exporter -} - -func (e *Exporter) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context { - switch { - case event.IsEnd(ev): - e.mu.Lock() - defer e.mu.Unlock() - span := export.GetSpan(ctx) - if span != nil { - e.spans = append(e.spans, span) - } - case event.IsMetric(ev): - e.mu.Lock() - defer e.mu.Unlock() - data := metric.Entries.Get(lm).([]metric.Data) - e.metrics = append(e.metrics, data...) - } - return ctx -} - -func (e *Exporter) Flush() { - e.mu.Lock() - defer e.mu.Unlock() - spans := make([]*wire.Span, len(e.spans)) - for i, s := range e.spans { - spans[i] = convertSpan(s) - } - e.spans = nil - metrics := make([]*wire.Metric, len(e.metrics)) - for i, m := range e.metrics { - metrics[i] = convertMetric(m, e.config.Start) - } - e.metrics = nil - - if len(spans) > 0 { - e.send("/v1/trace", &wire.ExportTraceServiceRequest{ - Node: e.config.buildNode(), - Spans: spans, - //TODO: Resource? - }) - } - if len(metrics) > 0 { - e.send("/v1/metrics", &wire.ExportMetricsServiceRequest{ - Node: e.config.buildNode(), - Metrics: metrics, - //TODO: Resource? - }) - } -} - -func (cfg *Config) buildNode() *wire.Node { - return &wire.Node{ - Identifier: &wire.ProcessIdentifier{ - HostName: cfg.Host, - Pid: cfg.Process, - StartTimestamp: convertTimestamp(cfg.Start), - }, - LibraryInfo: &wire.LibraryInfo{ - Language: wire.LanguageGo, - ExporterVersion: "0.0.1", - CoreLibraryVersion: "x/tools", - }, - ServiceInfo: &wire.ServiceInfo{ - Name: cfg.Service, - }, - } -} - -func (e *Exporter) send(endpoint string, message any) { - blob, err := json.Marshal(message) - if err != nil { - errorInExport("ocagent failed to marshal message for %v: %v", endpoint, err) - return - } - uri := e.config.Address + endpoint - req, err := http.NewRequest("POST", uri, bytes.NewReader(blob)) - if err != nil { - errorInExport("ocagent failed to build request for %v: %v", uri, err) - return - } - req.Header.Set("Content-Type", "application/json") - res, err := e.config.Client.Do(req) - if err != nil { - errorInExport("ocagent failed to send message: %v \n", err) - return - } - if res.Body != nil { - res.Body.Close() - } -} - -func errorInExport(message string, args ...any) { - // This function is useful when debugging the exporter, but in general we - // want to just drop any export -} - -func convertTimestamp(t time.Time) wire.Timestamp { - return t.Format(time.RFC3339Nano) -} - -func toTruncatableString(s string) *wire.TruncatableString { - if s == "" { - return nil - } - return &wire.TruncatableString{Value: s} -} - -func convertSpan(span *export.Span) *wire.Span { - result := &wire.Span{ - TraceID: span.ID.TraceID[:], - SpanID: span.ID.SpanID[:], - TraceState: nil, //TODO? - ParentSpanID: span.ParentID[:], - Name: toTruncatableString(span.Name), - Kind: wire.UnspecifiedSpanKind, - StartTime: convertTimestamp(span.Start().At()), - EndTime: convertTimestamp(span.Finish().At()), - Attributes: convertAttributes(span.Start(), 1), - TimeEvents: convertEvents(span.Events()), - SameProcessAsParentSpan: true, - //TODO: StackTrace? - //TODO: Links? - //TODO: Status? - //TODO: Resource? - } - return result -} - -func convertMetric(data metric.Data, start time.Time) *wire.Metric { - descriptor := dataToMetricDescriptor(data) - timeseries := dataToTimeseries(data, start) - - if descriptor == nil && timeseries == nil { - return nil - } - - // TODO: handle Histogram metrics - return &wire.Metric{ - MetricDescriptor: descriptor, - Timeseries: timeseries, - // TODO: attach Resource? - } -} - -func skipToValidLabel(list label.List, index int) (int, label.Label) { - // skip to the first valid label - for ; list.Valid(index); index++ { - l := list.Label(index) - if !l.Valid() || l.Key() == keys.Label { - continue - } - return index, l - } - return -1, label.Label{} -} - -func convertAttributes(list label.List, index int) *wire.Attributes { - index, l := skipToValidLabel(list, index) - if !l.Valid() { - return nil - } - attributes := make(map[string]wire.Attribute) - for { - if l.Valid() { - attributes[l.Key().Name()] = convertAttribute(l) - } - index++ - if !list.Valid(index) { - return &wire.Attributes{AttributeMap: attributes} - } - l = list.Label(index) - } -} - -func convertAttribute(l label.Label) wire.Attribute { - switch key := l.Key().(type) { - case *keys.Int: - return wire.IntAttribute{IntValue: int64(key.From(l))} - case *keys.Int8: - return wire.IntAttribute{IntValue: int64(key.From(l))} - case *keys.Int16: - return wire.IntAttribute{IntValue: int64(key.From(l))} - case *keys.Int32: - return wire.IntAttribute{IntValue: int64(key.From(l))} - case *keys.Int64: - return wire.IntAttribute{IntValue: int64(key.From(l))} - case *keys.UInt: - return wire.IntAttribute{IntValue: int64(key.From(l))} - case *keys.UInt8: - return wire.IntAttribute{IntValue: int64(key.From(l))} - case *keys.UInt16: - return wire.IntAttribute{IntValue: int64(key.From(l))} - case *keys.UInt32: - return wire.IntAttribute{IntValue: int64(key.From(l))} - case *keys.UInt64: - return wire.IntAttribute{IntValue: int64(key.From(l))} - case *keys.Float32: - return wire.DoubleAttribute{DoubleValue: float64(key.From(l))} - case *keys.Float64: - return wire.DoubleAttribute{DoubleValue: key.From(l)} - case *keys.Boolean: - return wire.BoolAttribute{BoolValue: key.From(l)} - case *keys.String: - return wire.StringAttribute{StringValue: toTruncatableString(key.From(l))} - case *keys.Error: - return wire.StringAttribute{StringValue: toTruncatableString(key.From(l).Error())} - case *keys.Value: - return wire.StringAttribute{StringValue: toTruncatableString(fmt.Sprint(key.From(l)))} - default: - return wire.StringAttribute{StringValue: toTruncatableString(fmt.Sprintf("%T", key))} - } -} - -func convertEvents(events []core.Event) *wire.TimeEvents { - //TODO: MessageEvents? - result := make([]wire.TimeEvent, len(events)) - for i, event := range events { - result[i] = convertEvent(event) - } - return &wire.TimeEvents{TimeEvent: result} -} - -func convertEvent(ev core.Event) wire.TimeEvent { - return wire.TimeEvent{ - Time: convertTimestamp(ev.At()), - Annotation: convertAnnotation(ev), - } -} - -func getAnnotationDescription(ev core.Event) (string, int) { - l := ev.Label(0) - if l.Key() != keys.Msg { - return "", 0 - } - if msg := keys.Msg.From(l); msg != "" { - return msg, 1 - } - l = ev.Label(1) - if l.Key() != keys.Err { - return "", 1 - } - if err := keys.Err.From(l); err != nil { - return err.Error(), 2 - } - return "", 2 -} - -func convertAnnotation(ev core.Event) *wire.Annotation { - description, index := getAnnotationDescription(ev) - if _, l := skipToValidLabel(ev, index); !l.Valid() && description == "" { - return nil - } - return &wire.Annotation{ - Description: toTruncatableString(description), - Attributes: convertAttributes(ev, index), - } -} diff --git a/internal/event/export/ocagent/ocagent_test.go b/internal/event/export/ocagent/ocagent_test.go deleted file mode 100644 index 38a52faede5..00000000000 --- a/internal/event/export/ocagent/ocagent_test.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package ocagent_test - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "sync" - "testing" - "time" - - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/core" - "golang.org/x/tools/internal/event/export" - "golang.org/x/tools/internal/event/export/metric" - "golang.org/x/tools/internal/event/export/ocagent" - "golang.org/x/tools/internal/event/keys" - "golang.org/x/tools/internal/event/label" -) - -const testNodeStr = `{ - "node":{ - "identifier":{ - "host_name":"tester", - "pid":1, - "start_timestamp":"1970-01-01T00:00:00Z" - }, - "library_info":{ - "language":4, - "exporter_version":"0.0.1", - "core_library_version":"x/tools" - }, - "service_info":{ - "name":"ocagent-tests" - } - },` - -var ( - keyDB = keys.NewString("db", "the database name") - keyMethod = keys.NewString("method", "a metric grouping key") - keyRoute = keys.NewString("route", "another metric grouping key") - - key1DB = keys.NewString("1_db", "A test string key") - - key2aAge = keys.NewFloat64("2a_age", "A test float64 key") - key2bTTL = keys.NewFloat32("2b_ttl", "A test float32 key") - key2cExpiryMS = keys.NewFloat64("2c_expiry_ms", "A test float64 key") - - key3aRetry = keys.NewBoolean("3a_retry", "A test boolean key") - key3bStale = keys.NewBoolean("3b_stale", "Another test boolean key") - - key4aMax = keys.NewInt("4a_max", "A test int key") - key4bOpcode = keys.NewInt8("4b_opcode", "A test int8 key") - key4cBase = keys.NewInt16("4c_base", "A test int16 key") - key4eChecksum = keys.NewInt32("4e_checksum", "A test int32 key") - key4fMode = keys.NewInt64("4f_mode", "A test int64 key") - - key5aMin = keys.NewUInt("5a_min", "A test uint key") - key5bMix = keys.NewUInt8("5b_mix", "A test uint8 key") - key5cPort = keys.NewUInt16("5c_port", "A test uint16 key") - key5dMinHops = keys.NewUInt32("5d_min_hops", "A test uint32 key") - key5eMaxHops = keys.NewUInt64("5e_max_hops", "A test uint64 key") - - recursiveCalls = keys.NewInt64("recursive_calls", "Number of recursive calls") - bytesIn = keys.NewInt64("bytes_in", "Number of bytes in") //, unit.Bytes) - latencyMs = keys.NewFloat64("latency", "The latency in milliseconds") //, unit.Milliseconds) - - metricLatency = metric.HistogramFloat64{ - Name: "latency_ms", - Description: "The latency of calls in milliseconds", - Keys: []label.Key{keyMethod, keyRoute}, - Buckets: []float64{0, 5, 10, 25, 50}, - } - - metricBytesIn = metric.HistogramInt64{ - Name: "latency_ms", - Description: "The latency of calls in milliseconds", - Keys: []label.Key{keyMethod, keyRoute}, - Buckets: []int64{0, 10, 50, 100, 500, 1000, 2000}, - } - - metricRecursiveCalls = metric.Scalar{ - Name: "latency_ms", - Description: "The latency of calls in milliseconds", - Keys: []label.Key{keyMethod, keyRoute}, - } -) - -type testExporter struct { - ocagent *ocagent.Exporter - sent fakeSender -} - -func registerExporter() *testExporter { - exporter := &testExporter{} - cfg := ocagent.Config{ - Host: "tester", - Process: 1, - Service: "ocagent-tests", - Client: &http.Client{Transport: &exporter.sent}, - } - cfg.Start, _ = time.Parse(time.RFC3339Nano, "1970-01-01T00:00:00Z") - exporter.ocagent = ocagent.Connect(&cfg) - - metrics := metric.Config{} - metricLatency.Record(&metrics, latencyMs) - metricBytesIn.Record(&metrics, bytesIn) - metricRecursiveCalls.SumInt64(&metrics, recursiveCalls) - - e := exporter.ocagent.ProcessEvent - e = metrics.Exporter(e) - e = spanFixer(e) - e = export.Spans(e) - e = export.Labels(e) - e = timeFixer(e) - event.SetExporter(e) - return exporter -} - -func timeFixer(output event.Exporter) event.Exporter { - start, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:30Z") - at, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:40Z") - end, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:50Z") - return func(ctx context.Context, ev core.Event, lm label.Map) context.Context { - switch { - case event.IsStart(ev): - ev = core.CloneEvent(ev, start) - case event.IsEnd(ev): - ev = core.CloneEvent(ev, end) - default: - ev = core.CloneEvent(ev, at) - } - return output(ctx, ev, lm) - } -} - -func spanFixer(output event.Exporter) event.Exporter { - return func(ctx context.Context, ev core.Event, lm label.Map) context.Context { - if event.IsStart(ev) { - span := export.GetSpan(ctx) - span.ID = export.SpanContext{} - } - return output(ctx, ev, lm) - } -} - -func (e *testExporter) Output(route string) []byte { - e.ocagent.Flush() - return e.sent.get(route) -} - -func checkJSON(t *testing.T, got, want []byte) { - // compare the compact form, to allow for formatting differences - g := &bytes.Buffer{} - if err := json.Compact(g, got); err != nil { - t.Fatal(err) - } - w := &bytes.Buffer{} - if err := json.Compact(w, want); err != nil { - t.Fatal(err) - } - if g.String() != w.String() { - t.Fatalf("Got:\n%s\nWant:\n%s", g, w) - } -} - -type fakeSender struct { - mu sync.Mutex - data map[string][]byte -} - -func (s *fakeSender) get(route string) []byte { - s.mu.Lock() - defer s.mu.Unlock() - data, found := s.data[route] - if found { - delete(s.data, route) - } - return data -} - -func (s *fakeSender) RoundTrip(req *http.Request) (*http.Response, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.data == nil { - s.data = make(map[string][]byte) - } - data, err := io.ReadAll(req.Body) - if err != nil { - return nil, err - } - path := req.URL.EscapedPath() - if _, found := s.data[path]; found { - return nil, fmt.Errorf("duplicate delivery to %v", path) - } - s.data[path] = data - return &http.Response{ - Status: "200 OK", - StatusCode: 200, - Proto: "HTTP/1.0", - ProtoMajor: 1, - ProtoMinor: 0, - }, nil -} diff --git a/internal/event/export/ocagent/trace_test.go b/internal/event/export/ocagent/trace_test.go deleted file mode 100644 index 99def34d149..00000000000 --- a/internal/event/export/ocagent/trace_test.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package ocagent_test - -import ( - "context" - "errors" - "testing" - - "golang.org/x/tools/internal/event" -) - -func TestTrace(t *testing.T) { - exporter := registerExporter() - const prefix = testNodeStr + ` - "spans":[{ - "trace_id":"AAAAAAAAAAAAAAAAAAAAAA==", - "span_id":"AAAAAAAAAAA=", - "parent_span_id":"AAAAAAAAAAA=", - "name":{"value":"event span"}, - "start_time":"1970-01-01T00:00:30Z", - "end_time":"1970-01-01T00:00:50Z", - "time_events":{ -` - const suffix = ` - }, - "same_process_as_parent_span":true - }] -}` - - tests := []struct { - name string - run func(ctx context.Context) - want string - }{ - { - name: "no labels", - run: func(ctx context.Context) { - event.Label(ctx) - }, - want: prefix + ` - "timeEvent":[{"time":"1970-01-01T00:00:40Z"}] - ` + suffix, - }, - { - name: "description no error", - run: func(ctx context.Context) { - event.Log(ctx, "cache miss", keyDB.Of("godb")) - }, - want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{ -"description": { "value": "cache miss" }, -"attributes": { - "attributeMap": { - "db": { "stringValue": { "value": "godb" } } - } -} -}}]` + suffix, - }, - - { - name: "description and error", - run: func(ctx context.Context) { - event.Error(ctx, "cache miss", - errors.New("no network connectivity"), - keyDB.Of("godb"), - ) - }, - want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{ -"description": { "value": "cache miss" }, -"attributes": { - "attributeMap": { - "db": { "stringValue": { "value": "godb" } }, - "error": { "stringValue": { "value": "no network connectivity" } } - } -} -}}]` + suffix, - }, - { - name: "no description, but error", - run: func(ctx context.Context) { - event.Error(ctx, "", - errors.New("no network connectivity"), - keyDB.Of("godb"), - ) - }, - want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{ -"description": { "value": "no network connectivity" }, -"attributes": { - "attributeMap": { - "db": { "stringValue": { "value": "godb" } } - } -} -}}]` + suffix, - }, - { - name: "enumerate all attribute types", - run: func(ctx context.Context) { - event.Log(ctx, "cache miss", - key1DB.Of("godb"), - - key2aAge.Of(0.456), // Constant converted into "float64" - key2bTTL.Of(float32(5000)), - key2cExpiryMS.Of(float64(1e3)), - - key3aRetry.Of(false), - key3bStale.Of(true), - - key4aMax.Of(0x7fff), // Constant converted into "int" - key4bOpcode.Of(int8(0x7e)), - key4cBase.Of(int16(1<<9)), - key4eChecksum.Of(int32(0x11f7e294)), - key4fMode.Of(int64(0644)), - - key5aMin.Of(uint(1)), - key5bMix.Of(uint8(44)), - key5cPort.Of(uint16(55678)), - key5dMinHops.Of(uint32(1<<9)), - key5eMaxHops.Of(uint64(0xffffff)), - ) - }, - want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{ -"description": { "value": "cache miss" }, -"attributes": { - "attributeMap": { - "1_db": { "stringValue": { "value": "godb" } }, - "2a_age": { "doubleValue": 0.456 }, - "2b_ttl": { "doubleValue": 5000 }, - "2c_expiry_ms": { "doubleValue": 1000 }, - "3a_retry": {}, - "3b_stale": { "boolValue": true }, - "4a_max": { "intValue": 32767 }, - "4b_opcode": { "intValue": 126 }, - "4c_base": { "intValue": 512 }, - "4e_checksum": { "intValue": 301458068 }, - "4f_mode": { "intValue": 420 }, - "5a_min": { "intValue": 1 }, - "5b_mix": { "intValue": 44 }, - "5c_port": { "intValue": 55678 }, - "5d_min_hops": { "intValue": 512 }, - "5e_max_hops": { "intValue": 16777215 } - } -} -}}]` + suffix, - }, - } - ctx := context.TODO() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx, done := event.Start(ctx, "event span") - tt.run(ctx) - done() - got := exporter.Output("/v1/trace") - checkJSON(t, got, []byte(tt.want)) - }) - } -} diff --git a/internal/event/export/ocagent/wire/common.go b/internal/event/export/ocagent/wire/common.go deleted file mode 100644 index f22b535654c..00000000000 --- a/internal/event/export/ocagent/wire/common.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package wire - -// This file holds common ocagent types - -type Node struct { - Identifier *ProcessIdentifier `json:"identifier,omitempty"` - LibraryInfo *LibraryInfo `json:"library_info,omitempty"` - ServiceInfo *ServiceInfo `json:"service_info,omitempty"` - Attributes map[string]string `json:"attributes,omitempty"` -} - -type Resource struct { - Type string `json:"type,omitempty"` - Labels map[string]string `json:"labels,omitempty"` -} - -type TruncatableString struct { - Value string `json:"value,omitempty"` - TruncatedByteCount int32 `json:"truncated_byte_count,omitempty"` -} - -type Attributes struct { - AttributeMap map[string]Attribute `json:"attributeMap,omitempty"` - DroppedAttributesCount int32 `json:"dropped_attributes_count,omitempty"` -} - -type StringAttribute struct { - StringValue *TruncatableString `json:"stringValue,omitempty"` -} - -type IntAttribute struct { - IntValue int64 `json:"intValue,omitempty"` -} - -type BoolAttribute struct { - BoolValue bool `json:"boolValue,omitempty"` -} - -type DoubleAttribute struct { - DoubleValue float64 `json:"doubleValue,omitempty"` -} - -type Attribute interface { - labelAttribute() -} - -func (StringAttribute) labelAttribute() {} -func (IntAttribute) labelAttribute() {} -func (BoolAttribute) labelAttribute() {} -func (DoubleAttribute) labelAttribute() {} - -type StackTrace struct { - StackFrames *StackFrames `json:"stack_frames,omitempty"` - StackTraceHashID uint64 `json:"stack_trace_hash_id,omitempty"` -} - -type StackFrames struct { - Frame []*StackFrame `json:"frame,omitempty"` - DroppedFramesCount int32 `json:"dropped_frames_count,omitempty"` -} - -type StackFrame struct { - FunctionName *TruncatableString `json:"function_name,omitempty"` - OriginalFunctionName *TruncatableString `json:"original_function_name,omitempty"` - FileName *TruncatableString `json:"file_name,omitempty"` - LineNumber int64 `json:"line_number,omitempty"` - ColumnNumber int64 `json:"column_number,omitempty"` - LoadModule *Module `json:"load_module,omitempty"` - SourceVersion *TruncatableString `json:"source_version,omitempty"` -} - -type Module struct { - Module *TruncatableString `json:"module,omitempty"` - BuildID *TruncatableString `json:"build_id,omitempty"` -} - -type ProcessIdentifier struct { - HostName string `json:"host_name,omitempty"` - Pid uint32 `json:"pid,omitempty"` - StartTimestamp Timestamp `json:"start_timestamp,omitempty"` -} - -type LibraryInfo struct { - Language Language `json:"language,omitempty"` - ExporterVersion string `json:"exporter_version,omitempty"` - CoreLibraryVersion string `json:"core_library_version,omitempty"` -} - -type Language int32 - -const ( - LanguageGo Language = 4 -) - -type ServiceInfo struct { - Name string `json:"name,omitempty"` -} diff --git a/internal/event/export/ocagent/wire/core.go b/internal/event/export/ocagent/wire/core.go deleted file mode 100644 index 95c05d66906..00000000000 --- a/internal/event/export/ocagent/wire/core.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package wire - -// This file contains type that match core proto types - -type Timestamp = string - -type Int64Value struct { - Value int64 `json:"value,omitempty"` -} - -type DoubleValue struct { - Value float64 `json:"value,omitempty"` -} diff --git a/internal/event/export/ocagent/wire/metrics.go b/internal/event/export/ocagent/wire/metrics.go deleted file mode 100644 index 6cb58943c00..00000000000 --- a/internal/event/export/ocagent/wire/metrics.go +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package wire - -import ( - "encoding/json" - "fmt" -) - -type ExportMetricsServiceRequest struct { - Node *Node `json:"node,omitempty"` - Metrics []*Metric `json:"metrics,omitempty"` - Resource *Resource `json:"resource,omitempty"` -} - -type Metric struct { - MetricDescriptor *MetricDescriptor `json:"metric_descriptor,omitempty"` - Timeseries []*TimeSeries `json:"timeseries,omitempty"` - Resource *Resource `json:"resource,omitempty"` -} - -type MetricDescriptor struct { - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Unit string `json:"unit,omitempty"` - Type MetricDescriptor_Type `json:"type,omitempty"` - LabelKeys []*LabelKey `json:"label_keys,omitempty"` -} - -type MetricDescriptor_Type int32 - -const ( - MetricDescriptor_UNSPECIFIED MetricDescriptor_Type = 0 - MetricDescriptor_GAUGE_INT64 MetricDescriptor_Type = 1 - MetricDescriptor_GAUGE_DOUBLE MetricDescriptor_Type = 2 - MetricDescriptor_GAUGE_DISTRIBUTION MetricDescriptor_Type = 3 - MetricDescriptor_CUMULATIVE_INT64 MetricDescriptor_Type = 4 - MetricDescriptor_CUMULATIVE_DOUBLE MetricDescriptor_Type = 5 - MetricDescriptor_CUMULATIVE_DISTRIBUTION MetricDescriptor_Type = 6 - MetricDescriptor_SUMMARY MetricDescriptor_Type = 7 -) - -type LabelKey struct { - Key string `json:"key,omitempty"` - Description string `json:"description,omitempty"` -} - -type TimeSeries struct { - StartTimestamp *Timestamp `json:"start_timestamp,omitempty"` - LabelValues []*LabelValue `json:"label_values,omitempty"` - Points []*Point `json:"points,omitempty"` -} - -type LabelValue struct { - Value string `json:"value,omitempty"` - HasValue bool `json:"has_value,omitempty"` -} - -type Point struct { - Timestamp *Timestamp `json:"timestamp,omitempty"` - Value PointValue `json:"value,omitempty"` -} - -type PointInt64Value struct { - Int64Value int64 `json:"int64Value,omitempty"` -} - -// MarshalJSON creates JSON formatted the same way as jsonpb so that the -// OpenCensus service can correctly determine the underlying value type. -// This custom MarshalJSON exists because, -// by default *Point is JSON marshalled as: -// -// {"value": {"int64Value": 1}} -// -// but it should be marshalled as: -// -// {"int64Value": 1} -func (p *Point) MarshalJSON() ([]byte, error) { - if p == nil { - return []byte("null"), nil - } - - switch d := p.Value.(type) { - case PointInt64Value: - return json.Marshal(&struct { - Timestamp *Timestamp `json:"timestamp,omitempty"` - Value int64 `json:"int64Value,omitempty"` - }{ - Timestamp: p.Timestamp, - Value: d.Int64Value, - }) - case PointDoubleValue: - return json.Marshal(&struct { - Timestamp *Timestamp `json:"timestamp,omitempty"` - Value float64 `json:"doubleValue,omitempty"` - }{ - Timestamp: p.Timestamp, - Value: d.DoubleValue, - }) - case PointDistributionValue: - return json.Marshal(&struct { - Timestamp *Timestamp `json:"timestamp,omitempty"` - Value *DistributionValue `json:"distributionValue,omitempty"` - }{ - Timestamp: p.Timestamp, - Value: d.DistributionValue, - }) - default: - return nil, fmt.Errorf("unknown point type %T", p.Value) - } -} - -type PointDoubleValue struct { - DoubleValue float64 `json:"doubleValue,omitempty"` -} - -type PointDistributionValue struct { - DistributionValue *DistributionValue `json:"distributionValue,omitempty"` -} - -type PointSummaryValue struct { - SummaryValue *SummaryValue `json:"summaryValue,omitempty"` -} - -type PointValue interface { - labelPointValue() -} - -func (PointInt64Value) labelPointValue() {} -func (PointDoubleValue) labelPointValue() {} -func (PointDistributionValue) labelPointValue() {} -func (PointSummaryValue) labelPointValue() {} - -type DistributionValue struct { - Count int64 `json:"count,omitempty"` - Sum float64 `json:"sum,omitempty"` - SumOfSquaredDeviation float64 `json:"sum_of_squared_deviation,omitempty"` - BucketOptions BucketOptions `json:"bucket_options,omitempty"` - Buckets []*Bucket `json:"buckets,omitempty"` -} - -type BucketOptionsExplicit struct { - Bounds []float64 `json:"bounds,omitempty"` -} - -type BucketOptions interface { - labelBucketOptions() -} - -func (*BucketOptionsExplicit) labelBucketOptions() {} - -var _ BucketOptions = (*BucketOptionsExplicit)(nil) -var _ json.Marshaler = (*BucketOptionsExplicit)(nil) - -// Declared for the purpose of custom JSON marshaling without cycles. -type bucketOptionsExplicitAlias BucketOptionsExplicit - -// MarshalJSON creates JSON formatted the same way as jsonpb so that the -// OpenCensus service can correctly determine the underlying value type. -// This custom MarshalJSON exists because, -// by default BucketOptionsExplicit is JSON marshalled as: -// -// {"bounds":[1,2,3]} -// -// but it should be marshalled as: -// -// {"explicit":{"bounds":[1,2,3]}} -func (be *BucketOptionsExplicit) MarshalJSON() ([]byte, error) { - return json.Marshal(&struct { - Explicit *bucketOptionsExplicitAlias `json:"explicit,omitempty"` - }{ - Explicit: (*bucketOptionsExplicitAlias)(be), - }) -} - -type Bucket struct { - Count int64 `json:"count,omitempty"` - Exemplar *Exemplar `json:"exemplar,omitempty"` -} - -type Exemplar struct { - Value float64 `json:"value,omitempty"` - Timestamp *Timestamp `json:"timestamp,omitempty"` - Attachments map[string]string `json:"attachments,omitempty"` -} - -type SummaryValue struct { - Count *Int64Value `json:"count,omitempty"` - Sum *DoubleValue `json:"sum,omitempty"` - Snapshot *Snapshot `json:"snapshot,omitempty"` -} - -type Snapshot struct { - Count *Int64Value `json:"count,omitempty"` - Sum *DoubleValue `json:"sum,omitempty"` - PercentileValues []*SnapshotValueAtPercentile `json:"percentile_values,omitempty"` -} - -type SnapshotValueAtPercentile struct { - Percentile float64 `json:"percentile,omitempty"` - Value float64 `json:"value,omitempty"` -} diff --git a/internal/event/export/ocagent/wire/metrics_test.go b/internal/event/export/ocagent/wire/metrics_test.go deleted file mode 100644 index 34247ad6332..00000000000 --- a/internal/event/export/ocagent/wire/metrics_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2020 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 wire - -import ( - "reflect" - "testing" -) - -func TestMarshalJSON(t *testing.T) { - tests := []struct { - name string - pt *Point - want string - }{ - { - "PointInt64", - &Point{ - Value: PointInt64Value{ - Int64Value: 5, - }, - }, - `{"int64Value":5}`, - }, - { - "PointDouble", - &Point{ - Value: PointDoubleValue{ - DoubleValue: 3.14, - }, - }, - `{"doubleValue":3.14}`, - }, - { - "PointDistribution", - &Point{ - Value: PointDistributionValue{ - DistributionValue: &DistributionValue{ - Count: 3, - Sum: 10, - Buckets: []*Bucket{ - { - Count: 1, - }, - { - Count: 2, - }, - }, - BucketOptions: &BucketOptionsExplicit{ - Bounds: []float64{ - 0, 5, - }, - }, - }, - }, - }, - `{"distributionValue":{"count":3,"sum":10,"bucket_options":{"explicit":{"bounds":[0,5]}},"buckets":[{"count":1},{"count":2}]}}`, - }, - { - "nil point", - nil, - `null`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - buf, err := tt.pt.MarshalJSON() - if err != nil { - t.Fatalf("Got:\n%v\nWant:\n%v", err, nil) - } - got := string(buf) - if !reflect.DeepEqual(got, tt.want) { - t.Fatalf("Got:\n%s\nWant:\n%s", got, tt.want) - } - }) - } -} diff --git a/internal/event/export/ocagent/wire/trace.go b/internal/event/export/ocagent/wire/trace.go deleted file mode 100644 index 88856673a18..00000000000 --- a/internal/event/export/ocagent/wire/trace.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package wire - -type ExportTraceServiceRequest struct { - Node *Node `json:"node,omitempty"` - Spans []*Span `json:"spans,omitempty"` - Resource *Resource `json:"resource,omitempty"` -} - -type Span struct { - TraceID []byte `json:"trace_id,omitempty"` - SpanID []byte `json:"span_id,omitempty"` - TraceState *TraceState `json:"tracestate,omitempty"` - ParentSpanID []byte `json:"parent_span_id,omitempty"` - Name *TruncatableString `json:"name,omitempty"` - Kind SpanKind `json:"kind,omitempty"` - StartTime Timestamp `json:"start_time,omitempty"` - EndTime Timestamp `json:"end_time,omitempty"` - Attributes *Attributes `json:"attributes,omitempty"` - StackTrace *StackTrace `json:"stack_trace,omitempty"` - TimeEvents *TimeEvents `json:"time_events,omitempty"` - Links *Links `json:"links,omitempty"` - Status *Status `json:"status,omitempty"` - Resource *Resource `json:"resource,omitempty"` - SameProcessAsParentSpan bool `json:"same_process_as_parent_span,omitempty"` - ChildSpanCount bool `json:"child_span_count,omitempty"` -} - -type TraceState struct { - Entries []*TraceStateEntry `json:"entries,omitempty"` -} - -type TraceStateEntry struct { - Key string `json:"key,omitempty"` - Value string `json:"value,omitempty"` -} - -type SpanKind int32 - -const ( - UnspecifiedSpanKind SpanKind = 0 - ServerSpanKind SpanKind = 1 - ClientSpanKind SpanKind = 2 -) - -type TimeEvents struct { - TimeEvent []TimeEvent `json:"timeEvent,omitempty"` - DroppedAnnotationsCount int32 `json:"dropped_annotations_count,omitempty"` - DroppedMessageEventsCount int32 `json:"dropped_message_events_count,omitempty"` -} - -type TimeEvent struct { - Time Timestamp `json:"time,omitempty"` - MessageEvent *MessageEvent `json:"messageEvent,omitempty"` - Annotation *Annotation `json:"annotation,omitempty"` -} - -type Annotation struct { - Description *TruncatableString `json:"description,omitempty"` - Attributes *Attributes `json:"attributes,omitempty"` -} - -type MessageEvent struct { - Type MessageEventType `json:"type,omitempty"` - ID uint64 `json:"id,omitempty"` - UncompressedSize uint64 `json:"uncompressed_size,omitempty"` - CompressedSize uint64 `json:"compressed_size,omitempty"` -} - -type MessageEventType int32 - -const ( - UnspecifiedMessageEvent MessageEventType = iota - SentMessageEvent - ReceivedMessageEvent -) - -type TimeEventValue interface { - labelTimeEventValue() -} - -func (Annotation) labelTimeEventValue() {} -func (MessageEvent) labelTimeEventValue() {} - -type Links struct { - Link []*Link `json:"link,omitempty"` - DroppedLinksCount int32 `json:"dropped_links_count,omitempty"` -} - -type Link struct { - TraceID []byte `json:"trace_id,omitempty"` - SpanID []byte `json:"span_id,omitempty"` - Type LinkType `json:"type,omitempty"` - Attributes *Attributes `json:"attributes,omitempty"` - TraceState *TraceState `json:"tracestate,omitempty"` -} - -type LinkType int32 - -const ( - UnspecifiedLinkType LinkType = 0 - ChildLinkType LinkType = 1 - ParentLinkType LinkType = 2 -) - -type Status struct { - Code int32 `json:"code,omitempty"` - Message string `json:"message,omitempty"` -} From ff03c59f3ffcb691d1205f8f2b57bcf992652358 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 27 Feb 2025 16:36:45 -0500 Subject: [PATCH 80/99] gopls/internal/analysis/modernize: append -> bytes.Clone This CL causes appendclipped to offer bytes.Clone in place of slices.Clone where the file already imports bytes but not slices. + test Updates golang/go#70815 Change-Id: I049698c3d5b8acf46abaa42ab34d72548a012a1a Reviewed-on: https://go-review.googlesource.com/c/tools/+/653455 LUCI-TryBot-Result: Go LUCI Commit-Queue: Alan Donovan Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- gopls/internal/analysis/modernize/slices.go | 33 ++++++++++++++++--- .../testdata/src/appendclipped/bytesclone.go | 11 +++++++ .../src/appendclipped/bytesclone.go.golden | 11 +++++++ 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go create mode 100644 gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go.golden diff --git a/gopls/internal/analysis/modernize/slices.go b/gopls/internal/analysis/modernize/slices.go index bdab9dea649..9cca3e98156 100644 --- a/gopls/internal/analysis/modernize/slices.go +++ b/gopls/internal/analysis/modernize/slices.go @@ -12,6 +12,7 @@ import ( "go/ast" "go/types" "slices" + "strconv" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -27,6 +28,10 @@ import ( // with a call to go1.21's slices.Concat(base, a, b, c), or simpler // replacements such as slices.Clone(a) in degenerate cases. // +// We offer bytes.Clone in preference to slices.Clone where +// appropriate, if the package already imports "bytes"; +// their behaviors are identical. +// // The base expression must denote a clipped slice (see [isClipped] // for definition), otherwise the replacement might eliminate intended // side effects to the base slice's array. @@ -41,7 +46,8 @@ import ( // The fix does not always preserve nilness the of base slice when the // addends (a, b, c) are all empty. func appendclipped(pass *analysis.Pass) { - if pass.Pkg.Path() == "slices" { + switch pass.Pkg.Path() { + case "slices", "bytes": return } @@ -94,15 +100,32 @@ func appendclipped(pass *analysis.Pass) { } } - // append(zerocap, s...) -> slices.Clone(s) - _, prefix, importEdits := analysisinternal.AddImport(info, file, "slices", "slices", "Clone", call.Pos()) + // If the slice type is []byte, and the file imports + // "bytes" but not "slices", prefer the (behaviorally + // identical) bytes.Clone for local consistency. + // https://go.dev/issue/70815#issuecomment-2671572984 + fileImports := func(path string) bool { + return slices.ContainsFunc(file.Imports, func(spec *ast.ImportSpec) bool { + value, _ := strconv.Unquote(spec.Path.Value) + return value == path + }) + } + clonepkg := cond( + types.Identical(info.TypeOf(call), byteSliceType) && + !fileImports("slices") && fileImports("bytes"), + "bytes", + "slices") + + // append(zerocap, s...) -> slices.Clone(s) or bytes.Clone(s) + _, prefix, importEdits := analysisinternal.AddImport(info, file, clonepkg, clonepkg, "Clone", call.Pos()) + message := fmt.Sprintf("Replace append with %s.Clone", clonepkg) pass.Report(analysis.Diagnostic{ Pos: call.Pos(), End: call.End(), Category: "slicesclone", - Message: "Replace append with slices.Clone", + Message: message, SuggestedFixes: []analysis.SuggestedFix{{ - Message: "Replace append with slices.Clone", + Message: message, TextEdits: append(importEdits, []analysis.TextEdit{{ Pos: call.Pos(), End: call.End(), diff --git a/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go b/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go new file mode 100644 index 00000000000..6425211b924 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go @@ -0,0 +1,11 @@ +package appendclipped + +import ( + "bytes" +) + +var _ bytes.Buffer + +func _(b []byte) { + print(append([]byte{}, b...)) // want "Replace append with bytes.Clone" +} diff --git a/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go.golden b/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go.golden new file mode 100644 index 00000000000..f49be6156b2 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/appendclipped/bytesclone.go.golden @@ -0,0 +1,11 @@ +package appendclipped + +import ( + "bytes" +) + +var _ bytes.Buffer + +func _(b []byte) { + print(bytes.Clone(b)) // want "Replace append with bytes.Clone" +} From 66eb306a364a3fd7c8ebb427be1425a3fd56262d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 25 Feb 2025 17:03:33 -0500 Subject: [PATCH 81/99] Revert "internal/settings: drop "annotations" setting" This reverts commit 5fe60fd (CL 639835), which removed the ability for users to customize the subset of "annotations" (a misnomer for categories of compiler optimization details). Apparently some users were relying on this experimental feature. Minor tweaks were made to comments but not to logic. Fixes golang/go#71888 Change-Id: I3d0227f841582a2cb29521b9b999546226b670ef Reviewed-on: https://go-review.googlesource.com/c/tools/+/652595 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- gopls/doc/settings.md | 24 ++++++++ gopls/internal/doc/api.json | 35 +++++++++++ gopls/internal/golang/compileropt.go | 72 +++++++++++++++------- gopls/internal/settings/default.go | 6 ++ gopls/internal/settings/settings.go | 77 +++++++++++++++++++++++- gopls/internal/settings/settings_test.go | 11 ++++ 6 files changed, 203 insertions(+), 22 deletions(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 7aeab79a575..1f4f5746524 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -355,6 +355,30 @@ These analyses are documented on Default: `false`. + +### `annotations map[enum]bool` + +annotations specifies the various kinds of compiler +optimization details that should be reported as diagnostics +when enabled for a package by the "Toggle compiler +optimization details" (`gopls.gc_details`) command. + +(Some users care only about one kind of annotation in their +profiling efforts. More importantly, in large packages, the +number of annotations can sometimes overwhelm the user +interface and exceed the per-file diagnostic limit.) + +TODO(adonovan): rename this field to CompilerOptDetail. + +Each enum must be one of: + +* `"bounds"` controls bounds checking diagnostics. +* `"escape"` controls diagnostics about escape choices. +* `"inline"` controls diagnostics about inlining choices. +* `"nil"` controls nil checks. + +Default: `{"bounds":true,"escape":true,"inline":true,"nil":true}`. + ### `vulncheck enum` diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index b6e53d18558..5775d0d4361 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -689,6 +689,41 @@ "Hierarchy": "ui.diagnostic", "DeprecationMessage": "" }, + { + "Name": "annotations", + "Type": "map[enum]bool", + "Doc": "annotations specifies the various kinds of compiler\noptimization details that should be reported as diagnostics\nwhen enabled for a package by the \"Toggle compiler\noptimization details\" (`gopls.gc_details`) command.\n\n(Some users care only about one kind of annotation in their\nprofiling efforts. More importantly, in large packages, the\nnumber of annotations can sometimes overwhelm the user\ninterface and exceed the per-file diagnostic limit.)\n\nTODO(adonovan): rename this field to CompilerOptDetail.\n", + "EnumKeys": { + "ValueType": "bool", + "Keys": [ + { + "Name": "\"bounds\"", + "Doc": "`\"bounds\"` controls bounds checking diagnostics.\n", + "Default": "true" + }, + { + "Name": "\"escape\"", + "Doc": "`\"escape\"` controls diagnostics about escape choices.\n", + "Default": "true" + }, + { + "Name": "\"inline\"", + "Doc": "`\"inline\"` controls diagnostics about inlining choices.\n", + "Default": "true" + }, + { + "Name": "\"nil\"", + "Doc": "`\"nil\"` controls nil checks.\n", + "Default": "true" + } + ] + }, + "EnumValues": null, + "Default": "{\"bounds\":true,\"escape\":true,\"inline\":true,\"nil\":true}", + "Status": "", + "Hierarchy": "ui.diagnostic", + "DeprecationMessage": "" + }, { "Name": "vulncheck", "Type": "enum", diff --git a/gopls/internal/golang/compileropt.go b/gopls/internal/golang/compileropt.go index f9f046463f6..bcce82c123f 100644 --- a/gopls/internal/golang/compileropt.go +++ b/gopls/internal/golang/compileropt.go @@ -16,6 +16,7 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/internal/event" ) @@ -65,7 +66,7 @@ func CompilerOptDetails(ctx context.Context, snapshot *cache.Snapshot, pkgDir pr reports := make(map[protocol.DocumentURI][]*cache.Diagnostic) var parseError error for _, fn := range files { - uri, diagnostics, err := parseDetailsFile(fn) + uri, diagnostics, err := parseDetailsFile(fn, snapshot.Options()) if err != nil { // expect errors for all the files, save 1 parseError = err @@ -87,7 +88,7 @@ func CompilerOptDetails(ctx context.Context, snapshot *cache.Snapshot, pkgDir pr } // parseDetailsFile parses the file written by the Go compiler which contains a JSON-encoded protocol.Diagnostic. -func parseDetailsFile(filename string) (protocol.DocumentURI, []*cache.Diagnostic, error) { +func parseDetailsFile(filename string, options *settings.Options) (protocol.DocumentURI, []*cache.Diagnostic, error) { buf, err := os.ReadFile(filename) if err != nil { return "", nil, err @@ -118,30 +119,14 @@ func parseDetailsFile(filename string) (protocol.DocumentURI, []*cache.Diagnosti if err := dec.Decode(d); err != nil { return "", nil, err } - if d.Source != "go compiler" { - continue - } d.Tags = []protocol.DiagnosticTag{} // must be an actual slice msg := d.Code.(string) if msg != "" { - // Typical message prefixes gathered by grepping the source of - // cmd/compile for literal arguments in calls to logopt.LogOpt. - // (It is not a well defined set.) - // - // - canInlineFunction - // - cannotInlineCall - // - cannotInlineFunction - // - copy - // - escape - // - escapes - // - isInBounds - // - isSliceInBounds - // - iteration-variable-to-{heap,stack} - // - leak - // - loop-modified-{range,for} - // - nilcheck msg = fmt.Sprintf("%s(%s)", msg, d.Message) } + if !showDiagnostic(msg, d.Source, options) { + continue + } // zeroIndexedRange subtracts 1 from the line and // range, because the compiler output neglects to @@ -186,6 +171,51 @@ func parseDetailsFile(filename string) (protocol.DocumentURI, []*cache.Diagnosti return uri, diagnostics, nil } +// showDiagnostic reports whether a given diagnostic should be shown to the end +// user, given the current options. +func showDiagnostic(msg, source string, o *settings.Options) bool { + if source != "go compiler" { + return false + } + if o.Annotations == nil { + return true + } + + // The strings below were gathered by grepping the source of + // cmd/compile for literal arguments in calls to logopt.LogOpt. + // (It is not a well defined set.) + // + // - canInlineFunction + // - cannotInlineCall + // - cannotInlineFunction + // - escape + // - escapes + // - isInBounds + // - isSliceInBounds + // - leak + // - nilcheck + // + // Additional ones not handled by logic below: + // - copy + // - iteration-variable-to-{heap,stack} + // - loop-modified-{range,for} + + switch { + case strings.HasPrefix(msg, "canInline") || + strings.HasPrefix(msg, "cannotInline") || + strings.HasPrefix(msg, "inlineCall"): + return o.Annotations[settings.Inline] + case strings.HasPrefix(msg, "escape") || msg == "leak": + return o.Annotations[settings.Escape] + case strings.HasPrefix(msg, "nilcheck"): + return o.Annotations[settings.Nil] + case strings.HasPrefix(msg, "isInBounds") || + strings.HasPrefix(msg, "isSliceInBounds"): + return o.Annotations[settings.Bounds] + } + return false +} + func findJSONFiles(dir string) ([]string, error) { ans := []string{} f := func(path string, fi os.FileInfo, _ error) error { diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index ebb3f1ccfae..aa81640f3e8 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go @@ -91,6 +91,12 @@ func DefaultOptions(overrides ...func(*Options)) *Options { }, UIOptions: UIOptions{ DiagnosticOptions: DiagnosticOptions{ + Annotations: map[Annotation]bool{ + Bounds: true, + Escape: true, + Inline: true, + Nil: true, + }, Vulncheck: ModeVulncheckOff, DiagnosticsDelay: 1 * time.Second, DiagnosticsTrigger: DiagnosticsOnEdit, diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 11b06040181..e98bc365935 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -18,6 +18,23 @@ import ( "golang.org/x/tools/gopls/internal/util/frob" ) +// An Annotation is a category of Go compiler optimization diagnostic. +type Annotation string + +const ( + // Nil controls nil checks. + Nil Annotation = "nil" + + // Escape controls diagnostics about escape choices. + Escape Annotation = "escape" + + // Inline controls diagnostics about inlining choices. + Inline Annotation = "inline" + + // Bounds controls bounds checking diagnostics. + Bounds Annotation = "bounds" +) + // Options holds various configuration that affects Gopls execution, organized // by the nature or origin of the settings. // @@ -436,6 +453,19 @@ type DiagnosticOptions struct { // [Staticcheck's website](https://staticcheck.io/docs/checks/). Staticcheck bool `status:"experimental"` + // Annotations specifies the various kinds of compiler + // optimization details that should be reported as diagnostics + // when enabled for a package by the "Toggle compiler + // optimization details" (`gopls.gc_details`) command. + // + // (Some users care only about one kind of annotation in their + // profiling efforts. More importantly, in large packages, the + // number of annotations can sometimes overwhelm the user + // interface and exceed the per-file diagnostic limit.) + // + // TODO(adonovan): rename this field to CompilerOptDetail. + Annotations map[Annotation]bool + // Vulncheck enables vulnerability scanning. Vulncheck VulncheckMode `status:"experimental"` @@ -1124,7 +1154,7 @@ func (o *Options) setOne(name string, value any) (applied []CounterPath, _ error return setBoolMap(&o.Hints, value) case "annotations": - return nil, &SoftError{"the 'annotations' setting was removed in gopls/v0.18.0; all compiler optimization details are now shown"} + return setAnnotationMap(&o.Annotations, value) case "vulncheck": return setEnum(&o.Vulncheck, value, @@ -1420,6 +1450,51 @@ func setDuration(dest *time.Duration, value any) error { return nil } +func setAnnotationMap(dest *map[Annotation]bool, value any) ([]CounterPath, error) { + all, err := asBoolMap[string](value) + if err != nil { + return nil, err + } + var counters []CounterPath + // Default to everything enabled by default. + m := make(map[Annotation]bool) + for k, enabled := range all { + var a Annotation + cnts, err := setEnum(&a, k, + Nil, + Escape, + Inline, + Bounds) + if err != nil { + // In case of an error, process any legacy values. + switch k { + case "noEscape": + m[Escape] = false + return nil, fmt.Errorf(`"noEscape" is deprecated, set "Escape: false" instead`) + + case "noNilcheck": + m[Nil] = false + return nil, fmt.Errorf(`"noNilcheck" is deprecated, set "Nil: false" instead`) + + case "noInline": + m[Inline] = false + return nil, fmt.Errorf(`"noInline" is deprecated, set "Inline: false" instead`) + + case "noBounds": + m[Bounds] = false + return nil, fmt.Errorf(`"noBounds" is deprecated, set "Bounds: false" instead`) + + default: + return nil, err + } + } + counters = append(counters, cnts...) + m[a] = enabled + } + *dest = m + return counters, nil +} + func setBoolMap[K ~string](dest *map[K]bool, value any) ([]CounterPath, error) { m, err := asBoolMap[K](value) if err != nil { diff --git a/gopls/internal/settings/settings_test.go b/gopls/internal/settings/settings_test.go index bd9ec110874..d7a032e1938 100644 --- a/gopls/internal/settings/settings_test.go +++ b/gopls/internal/settings/settings_test.go @@ -180,6 +180,17 @@ func TestOptions_Set(t *testing.T) { return len(o.DirectoryFilters) == 0 }, }, + { + name: "annotations", + value: map[string]any{ + "Nil": false, + "noBounds": true, + }, + wantError: true, + check: func(o Options) bool { + return !o.Annotations[Nil] && !o.Annotations[Bounds] + }, + }, { name: "vulncheck", value: []any{"invalid"}, From 408d2e2cc08b50104f3e92800ce7b74e7c89daa2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 28 Feb 2025 12:14:45 -0500 Subject: [PATCH 82/99] x/tools: remove workarounds for Go <1.23 Change-Id: I740769d6ed117bf140c9894b4464b3d3f7f326f1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/653655 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- cmd/deadcode/deadcode.go | 54 +++--------------------- go/analysis/analysistest/analysistest.go | 16 ++----- go/callgraph/vta/graph.go | 7 ++- go/callgraph/vta/propagation.go | 7 ++- go/callgraph/vta/propagation_test.go | 5 +-- go/callgraph/vta/utils.go | 7 ++- go/callgraph/vta/vta.go | 10 ++--- go/ssa/builder.go | 6 ++- go/ssa/lift.go | 19 +-------- go/types/internal/play/play.go | 9 ---- gopls/internal/cache/parsego/parse.go | 34 +-------------- internal/refactor/inline/inline.go | 35 +-------------- internal/refactor/inline/util.go | 10 ----- internal/typesinternal/element_test.go | 9 ++-- internal/typesinternal/zerovalue_test.go | 23 +++++----- 15 files changed, 48 insertions(+), 203 deletions(-) diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go index 0c66d07f79f..e164dc22ba8 100644 --- a/cmd/deadcode/deadcode.go +++ b/cmd/deadcode/deadcode.go @@ -15,11 +15,13 @@ import ( "go/types" "io" "log" + "maps" "os" "path/filepath" "regexp" "runtime" "runtime/pprof" + "slices" "sort" "strings" "text/template" @@ -290,9 +292,7 @@ func main() { // Build array of jsonPackage objects. var packages []any - pkgpaths := keys(byPkgPath) - sort.Strings(pkgpaths) - for _, pkgpath := range pkgpaths { + for _, pkgpath := range slices.Sorted(maps.Keys(byPkgPath)) { if !filter.MatchString(pkgpath) { continue } @@ -303,7 +303,7 @@ func main() { // declaration order. This tends to keep related // methods such as (T).Marshal and (*T).Unmarshal // together better than sorting. - fns := keys(m) + fns := slices.Collect(maps.Keys(m)) sort.Slice(fns, func(i, j int) bool { xposn := prog.Fset.Position(fns[i].Pos()) yposn := prog.Fset.Position(fns[j].Pos()) @@ -368,7 +368,7 @@ func prettyName(fn *ssa.Function, qualified bool) string { // anonymous? if fn.Parent() != nil { format(fn.Parent()) - i := index(fn.Parent().AnonFuncs, fn) + i := slices.Index(fn.Parent().AnonFuncs, fn) fmt.Fprintf(&buf, "$%d", i+1) return } @@ -427,7 +427,7 @@ func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Functio // Sort roots into preferred order. importsTesting := func(fn *ssa.Function) bool { isTesting := func(p *types.Package) bool { return p.Path() == "testing" } - return containsFunc(fn.Pkg.Pkg.Imports(), isTesting) + return slices.ContainsFunc(fn.Pkg.Pkg.Imports(), isTesting) } sort.Slice(roots, func(i, j int) bool { x, y := roots[i], roots[j] @@ -461,7 +461,7 @@ func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Functio for { edge := seen[node] if edge == nil { - reverse(path) + slices.Reverse(path) return path } path = append(path, edge) @@ -565,43 +565,3 @@ type jsonPosition struct { func (p jsonPosition) String() string { return fmt.Sprintf("%s:%d:%d", p.File, p.Line, p.Col) } - -// -- from the future -- - -// TODO(adonovan): use go1.22's slices and maps packages. - -func containsFunc[S ~[]E, E any](s S, f func(E) bool) bool { - return indexFunc(s, f) >= 0 -} - -func indexFunc[S ~[]E, E any](s S, f func(E) bool) int { - for i := range s { - if f(s[i]) { - return i - } - } - return -1 -} - -func index[S ~[]E, E comparable](s S, v E) int { - for i := range s { - if v == s[i] { - return i - } - } - return -1 -} - -func reverse[S ~[]E, E any](s S) { - for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { - s[i], s[j] = s[j], s[i] - } -} - -func keys[M ~map[K]V, K comparable, V any](m M) []K { - r := make([]K, 0, len(m)) - for k := range m { - r = append(r, k) - } - return r -} diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index 0b5cfe70bfe..143b4260346 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -7,12 +7,12 @@ package analysistest import ( "bytes" - "cmp" "fmt" "go/format" "go/token" "go/types" "log" + "maps" "os" "path/filepath" "regexp" @@ -215,7 +215,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns // Because the checking is driven by original // filenames, there is no way to express that a fix // (e.g. extract declaration) creates a new file. - for _, filename := range sortedKeys(allFilenames) { + for _, filename := range slices.Sorted(maps.Keys(allFilenames)) { // Read the original file. content, err := os.ReadFile(filename) if err != nil { @@ -266,7 +266,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns // Form #2: all suggested fixes are represented by a single file. want := ar.Comment var accumulated []diff.Edit - for _, message := range sortedKeys(fixEdits) { + for _, message := range slices.Sorted(maps.Keys(fixEdits)) { for _, fix := range fixEdits[message] { accumulated = merge(filename, message, accumulated, fix[filename]) } @@ -768,13 +768,3 @@ func sanitize(gopath, filename string) string { prefix := gopath + string(os.PathSeparator) + "src" + string(os.PathSeparator) return filepath.ToSlash(strings.TrimPrefix(filename, prefix)) } - -// TODO(adonovan): use better stuff from go1.23. -func sortedKeys[K cmp.Ordered, V any](m map[K]V) []K { - keys := make([]K, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - slices.Sort(keys) - return keys -} diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index c13b8a5e6cb..164018708ef 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -633,12 +633,12 @@ func (b *builder) call(c ssa.CallInstruction) { return } - siteCallees(c, b.callees)(func(f *ssa.Function) bool { + for f := range siteCallees(c, b.callees) { addArgumentFlows(b, c, f) site, ok := c.(ssa.Value) if !ok { - return true // go or defer + continue // go or defer } results := f.Signature.Results() @@ -653,8 +653,7 @@ func (b *builder) call(c ssa.CallInstruction) { b.addInFlowEdge(resultVar{f: f, index: i}, local) } } - return true - }) + } } func addArgumentFlows(b *builder, c ssa.CallInstruction, f *ssa.Function) { diff --git a/go/callgraph/vta/propagation.go b/go/callgraph/vta/propagation.go index 6ce1ca9e322..1c4dcd2888e 100644 --- a/go/callgraph/vta/propagation.go +++ b/go/callgraph/vta/propagation.go @@ -6,6 +6,7 @@ package vta import ( "go/types" + "iter" "slices" "golang.org/x/tools/go/callgraph/vta/internal/trie" @@ -113,11 +114,9 @@ type propType struct { // the role of a map from nodes to a set of propTypes. type propTypeMap map[node]*trie.MutMap -// propTypes returns a go1.23 iterator for the propTypes associated with +// propTypes returns an iterator for the propTypes associated with // node `n` in map `ptm`. -func (ptm propTypeMap) propTypes(n node) func(yield func(propType) bool) { - // TODO: when x/tools uses go1.23, change callers to use range-over-func - // (https://go.dev/issue/65237). +func (ptm propTypeMap) propTypes(n node) iter.Seq[propType] { return func(yield func(propType) bool) { if types := ptm[n]; types != nil { types.M.Range(func(_ uint64, elem any) bool { diff --git a/go/callgraph/vta/propagation_test.go b/go/callgraph/vta/propagation_test.go index 3885ef201cb..bc9ca1ecde6 100644 --- a/go/callgraph/vta/propagation_test.go +++ b/go/callgraph/vta/propagation_test.go @@ -98,10 +98,9 @@ func nodeToTypeString(pMap propTypeMap) map[string]string { nodeToTypeStr := make(map[string]string) for node := range pMap { var propStrings []string - pMap.propTypes(node)(func(prop propType) bool { + for prop := range pMap.propTypes(node) { propStrings = append(propStrings, propTypeString(prop)) - return true - }) + } sort.Strings(propStrings) nodeToTypeStr[node.String()] = strings.Join(propStrings, ";") } diff --git a/go/callgraph/vta/utils.go b/go/callgraph/vta/utils.go index bbd8400ec9b..3a708f220a7 100644 --- a/go/callgraph/vta/utils.go +++ b/go/callgraph/vta/utils.go @@ -6,6 +6,7 @@ package vta import ( "go/types" + "iter" "golang.org/x/tools/go/ssa" "golang.org/x/tools/internal/typeparams" @@ -147,10 +148,8 @@ func sliceArrayElem(t types.Type) types.Type { } } -// siteCallees returns a go1.23 iterator for the callees for call site `c`. -func siteCallees(c ssa.CallInstruction, callees calleesFunc) func(yield func(*ssa.Function) bool) { - // TODO: when x/tools uses go1.23, change callers to use range-over-func - // (https://go.dev/issue/65237). +// siteCallees returns an iterator for the callees for call site `c`. +func siteCallees(c ssa.CallInstruction, callees calleesFunc) iter.Seq[*ssa.Function] { return func(yield func(*ssa.Function) bool) { for _, callee := range callees(c) { if !yield(callee) { diff --git a/go/callgraph/vta/vta.go b/go/callgraph/vta/vta.go index 56fce13725f..ed12001fdb2 100644 --- a/go/callgraph/vta/vta.go +++ b/go/callgraph/vta/vta.go @@ -126,12 +126,11 @@ func (c *constructor) resolves(call ssa.CallInstruction) []*ssa.Function { // Cover the case of dynamic higher-order and interface calls. var res []*ssa.Function resolved := resolve(call, c.types, c.cache) - siteCallees(call, c.callees)(func(f *ssa.Function) bool { + for f := range siteCallees(call, c.callees) { if _, ok := resolved[f]; ok { res = append(res, f) } - return true - }) + } return res } @@ -140,12 +139,11 @@ func (c *constructor) resolves(call ssa.CallInstruction) []*ssa.Function { func resolve(c ssa.CallInstruction, types propTypeMap, cache methodCache) map[*ssa.Function]empty { fns := make(map[*ssa.Function]empty) n := local{val: c.Common().Value} - types.propTypes(n)(func(p propType) bool { + for p := range types.propTypes(n) { for _, f := range propFunc(p, c, cache) { fns[f] = empty{} } - return true - }) + } return fns } diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 1761dcc3068..84ccbc0927a 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -82,6 +82,8 @@ import ( "runtime" "sync" + "slices" + "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/versions" ) @@ -2021,8 +2023,8 @@ func (b *builder) forStmtGo122(fn *Function, s *ast.ForStmt, label *lblock) { // Remove instructions for phi, load, and store. // lift() will remove the unused i_next *Alloc. isDead := func(i Instruction) bool { return dead[i] } - loop.Instrs = removeInstrsIf(loop.Instrs, isDead) - post.Instrs = removeInstrsIf(post.Instrs, isDead) + loop.Instrs = slices.DeleteFunc(loop.Instrs, isDead) + post.Instrs = slices.DeleteFunc(post.Instrs, isDead) } } diff --git a/go/ssa/lift.go b/go/ssa/lift.go index aada3dc3227..6138ca82e0e 100644 --- a/go/ssa/lift.go +++ b/go/ssa/lift.go @@ -43,6 +43,7 @@ import ( "go/token" "math/big" "os" + "slices" "golang.org/x/tools/internal/typeparams" ) @@ -105,23 +106,7 @@ func buildDomFrontier(fn *Function) domFrontier { } func removeInstr(refs []Instruction, instr Instruction) []Instruction { - return removeInstrsIf(refs, func(i Instruction) bool { return i == instr }) -} - -func removeInstrsIf(refs []Instruction, p func(Instruction) bool) []Instruction { - // TODO(taking): replace with go1.22 slices.DeleteFunc. - i := 0 - for _, ref := range refs { - if p(ref) { - continue - } - refs[i] = ref - i++ - } - for j := i; j != len(refs); j++ { - refs[j] = nil // aid GC - } - return refs[:i] + return slices.DeleteFunc(refs, func(i Instruction) bool { return i == instr }) } // lift replaces local and new Allocs accessed only with diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go index 8d3b9d19346..f1318ac247a 100644 --- a/go/types/internal/play/play.go +++ b/go/types/internal/play/play.go @@ -430,12 +430,3 @@ textarea { width: 6in; } body { color: gray; } div#out { font-family: monospace; font-size: 80%; } ` - -// TODO(adonovan): use go1.21 built-in. -func min(x, y int) int { - if x < y { - return x - } else { - return y - } -} diff --git a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go index db6089d8e6d..08a1c395a2a 100644 --- a/gopls/internal/cache/parsego/parse.go +++ b/gopls/internal/cache/parsego/parse.go @@ -27,7 +27,6 @@ import ( "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/astutil" - "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/diff" @@ -65,39 +64,8 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s } // Inv: file != nil. - // Workaround for #70162 (missing File{Start,End} when - // parsing empty file) with go1.23. - // - // When parsing an empty file, or one without a valid - // package declaration, the go1.23 parser bails out before - // setting FileStart and End. - // - // This leaves us no way to find the original - // token.File that ParseFile created, so as a - // workaround, we recreate the token.File, and - // populate the FileStart and FileEnd fields. - // - // See also #53202. tokenFile := func(file *ast.File) *token.File { - tok := fset.File(file.FileStart) - if tok == nil { - // Invalid File.FileStart (also File.{Package,Name.Pos}). - if file.Package.IsValid() { - bug.Report("ast.File has valid Package but no FileStart") - } - if file.Name.Pos().IsValid() { - bug.Report("ast.File has valid Name.Pos but no FileStart") - } - tok = fset.AddFile(uri.Path(), -1, len(src)) - tok.SetLinesForContent(src) - // If the File contained any valid token.Pos values, - // they would all be invalid wrt the new token.File, - // but we have established that it lacks FileStart, - // Package, and Name.Pos. - file.FileStart = token.Pos(tok.Base()) - file.FileEnd = token.Pos(tok.Base() + tok.Size()) - } - return tok + return fset.File(file.FileStart) } tok := tokenFile(file) diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 6f6ed4583a9..2b6f06242e7 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -363,7 +363,7 @@ func (st *state) inline() (*Result, error) { specToDelete := oldImport.spec for _, decl := range f.Decls { if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT { - decl.Specs = slicesDeleteFunc(decl.Specs, func(spec ast.Spec) bool { + decl.Specs = slices.DeleteFunc(decl.Specs, func(spec ast.Spec) bool { imp := spec.(*ast.ImportSpec) // Since we re-parsed the file, we can't match by identity; // instead look for syntactic equivalence. @@ -2042,7 +2042,7 @@ func resolveEffects(logf logger, args []*argument, effects []int, sg substGraph) argi := args[i] if sg.has(argi) && !argi.pure { // i is not bound: check whether it must be bound due to hazards. - idx := index(effects, i) + idx := slices.Index(effects, i) if idx >= 0 { for _, j := range effects[:idx] { var ( @@ -3710,34 +3710,3 @@ func soleUse(info *types.Info, obj types.Object) (sole *ast.Ident) { } type unit struct{} // for representing sets as maps - -// slicesDeleteFunc removes any elements from s for which del returns true, -// returning the modified slice. -// slicesDeleteFunc zeroes the elements between the new length and the original length. -// TODO(adonovan): use go1.21 slices.DeleteFunc -func slicesDeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { - i := slicesIndexFunc(s, del) - if i == -1 { - return s - } - // Don't start copying elements until we find one to delete. - for j := i + 1; j < len(s); j++ { - if v := s[j]; !del(v) { - s[i] = v - i++ - } - } - // clear(s[i:]) // zero/nil out the obsolete elements, for GC - return s[:i] -} - -// slicesIndexFunc returns the first index i satisfying f(s[i]), -// or -1 if none do. -func slicesIndexFunc[S ~[]E, E any](s S, f func(E) bool) int { - for i := range s { - if f(s[i]) { - return i - } - } - return -1 -} diff --git a/internal/refactor/inline/util.go b/internal/refactor/inline/util.go index 591dc4265c0..c3f049c73b0 100644 --- a/internal/refactor/inline/util.go +++ b/internal/refactor/inline/util.go @@ -22,16 +22,6 @@ func is[T any](x any) bool { return ok } -// TODO(adonovan): use go1.21's slices.Index. -func index[T comparable](slice []T, x T) int { - for i, elem := range slice { - if elem == x { - return i - } - } - return -1 -} - func btoi(b bool) int { if b { return 1 diff --git a/internal/typesinternal/element_test.go b/internal/typesinternal/element_test.go index b4475633270..95f1ab33478 100644 --- a/internal/typesinternal/element_test.go +++ b/internal/typesinternal/element_test.go @@ -9,6 +9,8 @@ import ( "go/parser" "go/token" "go/types" + "maps" + "slices" "strings" "testing" @@ -142,12 +144,7 @@ func TestForEachElement(t *testing.T) { } } if fail { - for k := range got { - t.Logf("got element: %s", k) - } - // TODO(adonovan): use this when go1.23 is assured: - // t.Logf("got elements:\n%s", - // strings.Join(slices.Sorted(maps.Keys(got)), "\n")) + t.Logf("got elements:\n%s", strings.Join(slices.Sorted(maps.Keys(got)), "\n")) } } } diff --git a/internal/typesinternal/zerovalue_test.go b/internal/typesinternal/zerovalue_test.go index 8ec1012dfda..67295a95020 100644 --- a/internal/typesinternal/zerovalue_test.go +++ b/internal/typesinternal/zerovalue_test.go @@ -68,15 +68,15 @@ type aliasNamed = foo func _[T any]() { type aliasTypeParam = T - // type aliasWithTypeParam[u any] = struct { - // x u - // y T - // } - // type aliasWithTypeParams[u, q any] = struct { - // x u - // y q - // z T - // } + type aliasWithTypeParam[u any] = struct { + x u + y T + } + type aliasWithTypeParams[u, q any] = struct { + x u + y q + z T + } type namedWithTypeParam[u any] struct { x u @@ -135,9 +135,8 @@ func _[T any]() { _ aliasTypeParam // *new(T) _ *aliasTypeParam // nil - // TODO(hxjiang): add test for alias type param after stop supporting go1.22. - // _ aliasWithTypeParam[int] // aliasWithTypeParam[int]{} - // _ aliasWithTypeParams[int, string] // aliasWithTypeParams[int, string]{} + _ aliasWithTypeParam[int] // aliasWithTypeParam[int]{} + _ aliasWithTypeParams[int, string] // aliasWithTypeParams[int, string]{} _ namedWithTypeParam[int] // namedWithTypeParam[int]{} _ namedWithTypeParams[int, string] // namedWithTypeParams[int, string]{} From 608d370dd53cb3898f3ddb6dfa5f0d29eae80d2d Mon Sep 17 00:00:00 2001 From: cuishuang Date: Thu, 27 Feb 2025 12:49:28 +0800 Subject: [PATCH 83/99] internal/imports: use a more straightforward return value Change-Id: Ibd8249da636a854dd1a53c047e3d215ef45c911f Reviewed-on: https://go-review.googlesource.com/c/tools/+/653196 Reviewed-by: Michael Pratt LUCI-TryBot-Result: Go LUCI Reviewed-by: Ian Lance Taylor Auto-Submit: Ian Lance Taylor --- internal/imports/fix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/imports/fix.go b/internal/imports/fix.go index ee0efe48a55..737a9bfae8f 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go @@ -559,7 +559,7 @@ func fixImportsDefault(fset *token.FileSet, f *ast.File, filename string, env *P return err } apply(fset, f, fixes) - return err + return nil } // getFixes gets the import fixes that need to be made to f in order to fix the imports. From b2aa62b57015c812848d950d884f626839a43fd7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 27 Feb 2025 16:03:51 -0500 Subject: [PATCH 84/99] internal/stdlib: provide API for import graph of std library This CL adds two functions for accessing the direct and transitive imports of the packages of the standard library: func Imports(pkgs ...string) iter.Seq[string] func Dependencies(pkgs ...string) iter.Seq[string] These are needed by modernizers so that they can avoid offering fixes that add an import of, say, "slices" while analyzing a package that is itself a dependency of "slices". The compressed graph is generated from the current toolchain; this may not exactly match the source code being analyzed by the application, but we expect drift to be small. Updates golang/go#70815 Change-Id: I2d7180bcff1d1c72ce61b8436a346b8921c02ba9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/653356 Commit-Queue: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Reviewed-by: Ian Lance Taylor --- internal/stdlib/deps.go | 359 +++++++++++++++++++++++ internal/stdlib/deps_test.go | 36 +++ internal/stdlib/generate.go | 125 +++++++- internal/stdlib/import.go | 89 ++++++ internal/stdlib/manifest.go | 9 +- internal/stdlib/stdlib.go | 2 +- internal/stdlib/testdata/nethttp.deps | 171 +++++++++++ internal/stdlib/testdata/nethttp.imports | 47 +++ 8 files changed, 834 insertions(+), 4 deletions(-) create mode 100644 internal/stdlib/deps.go create mode 100644 internal/stdlib/deps_test.go create mode 100644 internal/stdlib/import.go create mode 100644 internal/stdlib/testdata/nethttp.deps create mode 100644 internal/stdlib/testdata/nethttp.imports diff --git a/internal/stdlib/deps.go b/internal/stdlib/deps.go new file mode 100644 index 00000000000..7cca431cd65 --- /dev/null +++ b/internal/stdlib/deps.go @@ -0,0 +1,359 @@ +// 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. + +// Code generated by generate.go. DO NOT EDIT. + +package stdlib + +type pkginfo struct { + name string + deps string // list of indices of dependencies, as varint-encoded deltas +} + +var deps = [...]pkginfo{ + {"archive/tar", "\x03k\x03E5\x01\v\x01#\x01\x01\x02\x05\t\x02\x01\x02\x02\v"}, + {"archive/zip", "\x02\x04a\a\x16\x0205\x01+\x05\x01\x10\x03\x02\r\x04"}, + {"bufio", "\x03k}E\x13"}, + {"bytes", "n+R\x03\fG\x02\x02"}, + {"cmp", ""}, + {"compress/bzip2", "\x02\x02\xe7\x01B"}, + {"compress/flate", "\x02l\x03z\r\x024\x01\x03"}, + {"compress/gzip", "\x02\x04a\a\x03\x15eT"}, + {"compress/lzw", "\x02l\x03z"}, + {"compress/zlib", "\x02\x04a\a\x03\x13\x01f"}, + {"container/heap", "\xae\x02"}, + {"container/list", ""}, + {"container/ring", ""}, + {"context", "n\\h\x01\f"}, + {"crypto", "\x84\x01gD"}, + {"crypto/aes", "\x10\n\a\x8e\x02"}, + {"crypto/cipher", "\x03\x1e\x01\x01\x1d\x11\x1d,Q"}, + {"crypto/des", "\x10\x13\x1d.,\x95\x01\x03"}, + {"crypto/dsa", "@\x04*}\x0e"}, + {"crypto/ecdh", "\x03\v\f\x0e\x04\x14\x04\r\x1d}"}, + {"crypto/ecdsa", "\x0e\x05\x03\x04\x01\x0e\x16\x01\x04\f\x01\x1d}\x0e\x04K\x01"}, + {"crypto/ed25519", "\x0e\x1c\x16\n\a\x1d}D"}, + {"crypto/elliptic", "0>}\x0e9"}, + {"crypto/fips140", " \x05\x91\x01"}, + {"crypto/hkdf", "-\x12\x01.\x16"}, + {"crypto/hmac", "\x1a\x14\x11\x01\x113"}, + {"crypto/internal/boring", "\x0e\x02\rg"}, + {"crypto/internal/boring/bbig", "\x1a\xdf\x01L"}, + {"crypto/internal/boring/bcache", "\xb3\x02\x12"}, + {"crypto/internal/boring/sig", ""}, + {"crypto/internal/cryptotest", "\x03\r\n)\x0e\x1a\x06\x13\x12#\a\t\x11\x11\x11\x1b\x01\f\f\x05\n"}, + {"crypto/internal/entropy", "E"}, + {"crypto/internal/fips140", ">0}9\f\x15"}, + {"crypto/internal/fips140/aes", "\x03\x1d\x03\x02\x13\x04\x01\x01\x05+\x8c\x015"}, + {"crypto/internal/fips140/aes/gcm", " \x01\x02\x02\x02\x11\x04\x01\x06+\x8a\x01"}, + {"crypto/internal/fips140/alias", "\xc5\x02"}, + {"crypto/internal/fips140/bigmod", "%\x17\x01\x06+\x8c\x01"}, + {"crypto/internal/fips140/check", " \x0e\x06\b\x02\xad\x01Z"}, + {"crypto/internal/fips140/check/checktest", "%\xff\x01!"}, + {"crypto/internal/fips140/drbg", "\x03\x1c\x01\x01\x04\x13\x04\b\x01)}\x0f8"}, + {"crypto/internal/fips140/ecdh", "\x03\x1d\x05\x02\t\f2}\x0f8"}, + {"crypto/internal/fips140/ecdsa", "\x03\x1d\x04\x01\x02\a\x02\x068}G"}, + {"crypto/internal/fips140/ed25519", "\x03\x1d\x05\x02\x04\v8\xc1\x01\x03"}, + {"crypto/internal/fips140/edwards25519", "%\a\f\x042\x8c\x018"}, + {"crypto/internal/fips140/edwards25519/field", "%\x13\x042\x8c\x01"}, + {"crypto/internal/fips140/hkdf", "\x03\x1d\x05\t\x06:"}, + {"crypto/internal/fips140/hmac", "\x03\x1d\x14\x01\x018"}, + {"crypto/internal/fips140/mlkem", "\x03\x1d\x05\x02\x0e\x03\x042"}, + {"crypto/internal/fips140/nistec", "%\f\a\x042\x8c\x01*\x0e\x13"}, + {"crypto/internal/fips140/nistec/fiat", "%\x136\x8c\x01"}, + {"crypto/internal/fips140/pbkdf2", "\x03\x1d\x05\t\x06:"}, + {"crypto/internal/fips140/rsa", "\x03\x1d\x04\x01\x02\r\x01\x01\x026}G"}, + {"crypto/internal/fips140/sha256", "\x03\x1d\x1c\x01\x06+\x8c\x01"}, + {"crypto/internal/fips140/sha3", "\x03\x1d\x18\x04\x011\x8c\x01K"}, + {"crypto/internal/fips140/sha512", "\x03\x1d\x1c\x01\x06+\x8c\x01"}, + {"crypto/internal/fips140/ssh", " \x05"}, + {"crypto/internal/fips140/subtle", "#\x19\xbe\x01"}, + {"crypto/internal/fips140/tls12", "\x03\x1d\x05\t\x06\x028"}, + {"crypto/internal/fips140/tls13", "\x03\x1d\x05\b\a\b2"}, + {"crypto/internal/fips140deps", ""}, + {"crypto/internal/fips140deps/byteorder", "\x9a\x01"}, + {"crypto/internal/fips140deps/cpu", "\xae\x01\a"}, + {"crypto/internal/fips140deps/godebug", "\xb6\x01"}, + {"crypto/internal/fips140hash", "5\x1a5\xc1\x01"}, + {"crypto/internal/fips140only", "'\r\x01\x01N25"}, + {"crypto/internal/fips140test", ""}, + {"crypto/internal/hpke", "\x0e\x01\x01\x03\x1a\x1d$,`M"}, + {"crypto/internal/impl", "\xb0\x02"}, + {"crypto/internal/randutil", "\xeb\x01\x12"}, + {"crypto/internal/sysrand", "\xd7\x01@\x1b\x01\f\x06"}, + {"crypto/internal/sysrand/internal/seccomp", "n"}, + {"crypto/md5", "\x0e2.\x16\x16`"}, + {"crypto/mlkem", "/"}, + {"crypto/pbkdf2", "2\r\x01.\x16"}, + {"crypto/rand", "\x1a\x06\a\x19\x04\x01)}\x0eL"}, + {"crypto/rc4", "#\x1d.\xc1\x01"}, + {"crypto/rsa", "\x0e\f\x01\t\x0f\f\x01\x04\x06\a\x1d\x03\x1325\r\x01"}, + {"crypto/sha1", "\x0e\f&.\x16\x16\x14L"}, + {"crypto/sha256", "\x0e\f\x1aP"}, + {"crypto/sha3", "\x0e'O\xc1\x01"}, + {"crypto/sha512", "\x0e\f\x1cN"}, + {"crypto/subtle", "8\x98\x01T"}, + {"crypto/tls", "\x03\b\x02\x01\x01\x01\x01\x02\x01\x01\x01\x03\x01\a\x01\v\x02\n\x01\b\x05\x03\x01\x01\x01\x01\x02\x01\x02\x01\x18\x02\x03\x13\x16\x14\b5\x16\x16\r\t\x01\x01\x01\x02\x01\f\x06\x02\x01"}, + {"crypto/tls/internal/fips140tls", " \x93\x02"}, + {"crypto/x509", "\x03\v\x01\x01\x01\x01\x01\x01\x01\x011\x03\x02\x01\x01\x02\x05\x01\x0e\x06\x02\x02\x03E5\x03\t\x01\x01\x01\a\x10\x05\t\x05\v\x01\x02\r\x02\x01\x01\x02\x03\x01"}, + {"crypto/x509/internal/macos", "\x03k'\x8f\x01\v\x10\x06"}, + {"crypto/x509/pkix", "d\x06\a\x88\x01F"}, + {"database/sql", "\x03\nK\x16\x03z\f\x06\"\x05\t\x02\x03\x01\f\x02\x02\x02"}, + {"database/sql/driver", "\ra\x03\xae\x01\x10\x10"}, + {"debug/buildinfo", "\x03X\x02\x01\x01\b\a\x03`\x18\x02\x01+\x10\x1e"}, + {"debug/dwarf", "\x03d\a\x03z1\x12\x01\x01"}, + {"debug/elf", "\x03\x06Q\r\a\x03`\x19\x01,\x18\x01\x15"}, + {"debug/gosym", "\x03d\n\xbd\x01\x01\x01\x02"}, + {"debug/macho", "\x03\x06Q\r\n`\x1a,\x18\x01"}, + {"debug/pe", "\x03\x06Q\r\a\x03`\x1a,\x18\x01\x15"}, + {"debug/plan9obj", "g\a\x03`\x1a,"}, + {"embed", "n+:\x18\x01S"}, + {"embed/internal/embedtest", ""}, + {"encoding", ""}, + {"encoding/ascii85", "\xeb\x01D"}, + {"encoding/asn1", "\x03k\x03\x87\x01\x01&\x0e\x02\x01\x0f\x03\x01"}, + {"encoding/base32", "\xeb\x01B\x02"}, + {"encoding/base64", "\x9a\x01QB\x02"}, + {"encoding/binary", "n}\r'\x0e\x05"}, + {"encoding/csv", "\x02\x01k\x03zE\x11\x02"}, + {"encoding/gob", "\x02`\x05\a\x03`\x1a\f\x01\x02\x1d\b\x13\x01\x0e\x02"}, + {"encoding/hex", "n\x03zB\x03"}, + {"encoding/json", "\x03\x01^\x04\b\x03z\r'\x0e\x02\x01\x02\x0f\x01\x01\x02"}, + {"encoding/pem", "\x03c\b}B\x03"}, + {"encoding/xml", "\x02\x01_\f\x03z4\x05\v\x01\x02\x0f\x02"}, + {"errors", "\xca\x01{"}, + {"expvar", "kK9\t\n\x15\r\t\x02\x03\x01\x10"}, + {"flag", "b\f\x03z,\b\x05\t\x02\x01\x0f"}, + {"fmt", "nE8\r\x1f\b\x0e\x02\x03\x11"}, + {"go/ast", "\x03\x01m\x0f\x01j\x03)\b\x0e\x02\x01"}, + {"go/ast/internal/tests", ""}, + {"go/build", "\x02\x01k\x03\x01\x03\x02\a\x02\x01\x17\x1e\x04\x02\t\x14\x12\x01+\x01\x04\x01\a\t\x02\x01\x11\x02\x02"}, + {"go/build/constraint", "n\xc1\x01\x01\x11\x02"}, + {"go/constant", "q\x10w\x01\x015\x01\x02\x11"}, + {"go/doc", "\x04m\x01\x06\t=-1\x11\x02\x01\x11\x02"}, + {"go/doc/comment", "\x03n\xbc\x01\x01\x01\x01\x11\x02"}, + {"go/format", "\x03n\x01\f\x01\x02jE"}, + {"go/importer", "t\a\x01\x01\x04\x01i9"}, + {"go/internal/gccgoimporter", "\x02\x01X\x13\x03\x05\v\x01g\x02,\x01\x05\x12\x01\v\b"}, + {"go/internal/gcimporter", "\x02o\x10\x01/\x05\x0e',\x16\x03\x02"}, + {"go/internal/srcimporter", "q\x01\x02\n\x03\x01i,\x01\x05\x13\x02\x13"}, + {"go/parser", "\x03k\x03\x01\x03\v\x01j\x01+\x06\x13"}, + {"go/printer", "q\x01\x03\x03\tj\r\x1f\x16\x02\x01\x02\n\x05\x02"}, + {"go/scanner", "\x03n\x10j2\x11\x01\x12\x02"}, + {"go/token", "\x04m\xbc\x01\x02\x03\x01\x0e\x02"}, + {"go/types", "\x03\x01\x06d\x03\x01\x04\b\x03\x02\x15\x1e\x06+\x04\x03\n%\a\t\x01\x01\x01\x02\x01\x0e\x02\x02"}, + {"go/version", "\xbb\x01u"}, + {"hash", "\xeb\x01"}, + {"hash/adler32", "n\x16\x16"}, + {"hash/crc32", "n\x16\x16\x14\x84\x01\x01"}, + {"hash/crc64", "n\x16\x16\x98\x01"}, + {"hash/fnv", "n\x16\x16`"}, + {"hash/maphash", "\x95\x01\x05\x1b\x03@M"}, + {"html", "\xb0\x02\x02\x11"}, + {"html/template", "\x03h\x06\x19,5\x01\v \x05\x01\x02\x03\r\x01\x02\v\x01\x03\x02"}, + {"image", "\x02l\x1f^\x0f5\x03\x01"}, + {"image/color", ""}, + {"image/color/palette", "\x8d\x01"}, + {"image/draw", "\x8c\x01\x01\x04"}, + {"image/gif", "\x02\x01\x05f\x03\x1b\x01\x01\x01\vQ"}, + {"image/internal/imageutil", "\x8c\x01"}, + {"image/jpeg", "\x02l\x1e\x01\x04Z"}, + {"image/png", "\x02\a^\n\x13\x02\x06\x01^D"}, + {"index/suffixarray", "\x03d\a}\r*\v\x01"}, + {"internal/abi", "\xb5\x01\x90\x01"}, + {"internal/asan", "\xc5\x02"}, + {"internal/bisect", "\xa4\x02\x0e\x01"}, + {"internal/buildcfg", "qG_\x06\x02\x05\v\x01"}, + {"internal/bytealg", "\xae\x01\x97\x01"}, + {"internal/byteorder", ""}, + {"internal/cfg", ""}, + {"internal/chacha8rand", "\x9a\x01\x1b\x90\x01"}, + {"internal/copyright", ""}, + {"internal/coverage", ""}, + {"internal/coverage/calloc", ""}, + {"internal/coverage/cfile", "k\x06\x17\x16\x01\x02\x01\x01\x01\x01\x01\x01\x01$\x01\x1e,\x06\a\v\x01\x03\f\x06"}, + {"internal/coverage/cformat", "\x04m-\x04I\f6\x01\x02\f"}, + {"internal/coverage/cmerge", "q-Z"}, + {"internal/coverage/decodecounter", "g\n-\v\x02@,\x18\x16"}, + {"internal/coverage/decodemeta", "\x02e\n\x17\x16\v\x02@,"}, + {"internal/coverage/encodecounter", "\x02e\n-\f\x01\x02>\f \x16"}, + {"internal/coverage/encodemeta", "\x02\x01d\n\x13\x04\x16\r\x02>,."}, + {"internal/coverage/pods", "\x04m-y\x06\x05\v\x02\x01"}, + {"internal/coverage/rtcov", "\xc5\x02"}, + {"internal/coverage/slicereader", "g\nzZ"}, + {"internal/coverage/slicewriter", "qz"}, + {"internal/coverage/stringtab", "q8\x04>"}, + {"internal/coverage/test", ""}, + {"internal/coverage/uleb128", ""}, + {"internal/cpu", "\xc5\x02"}, + {"internal/dag", "\x04m\xbc\x01\x03"}, + {"internal/diff", "\x03n\xbd\x01\x02"}, + {"internal/exportdata", "\x02\x01k\x03\x03]\x1a,\x01\x05\x12\x01\x02"}, + {"internal/filepathlite", "n+:\x19A"}, + {"internal/fmtsort", "\x04\x9b\x02\x0e"}, + {"internal/fuzz", "\x03\nA\x19\x04\x03\x03\x01\f\x0355\r\x02\x1d\x01\x05\x02\x05\v\x01\x02\x01\x01\v\x04\x02"}, + {"internal/goarch", ""}, + {"internal/godebug", "\x97\x01 {\x01\x12"}, + {"internal/godebugs", ""}, + {"internal/goexperiment", ""}, + {"internal/goos", ""}, + {"internal/goroot", "\x97\x02\x01\x05\x13\x02"}, + {"internal/gover", "\x04"}, + {"internal/goversion", ""}, + {"internal/itoa", ""}, + {"internal/lazyregexp", "\x97\x02\v\x0e\x02"}, + {"internal/lazytemplate", "\xeb\x01,\x19\x02\v"}, + {"internal/msan", "\xc5\x02"}, + {"internal/nettrace", ""}, + {"internal/obscuretestdata", "f\x85\x01,"}, + {"internal/oserror", "n"}, + {"internal/pkgbits", "\x03K\x19\a\x03\x05\vj\x0e\x1e\r\v\x01"}, + {"internal/platform", ""}, + {"internal/poll", "nO\x1a\x149\x0e\x01\x01\v\x06"}, + {"internal/profile", "\x03\x04g\x03z7\f\x01\x01\x0f"}, + {"internal/profilerecord", ""}, + {"internal/race", "\x95\x01\xb0\x01"}, + {"internal/reflectlite", "\x95\x01 3\x01P\x0e\x13\x12"}, + {"unsafe", ""}, + {"vendor/golang.org/x/crypto/chacha20", "\x10W\a\x8c\x01*&"}, + {"vendor/golang.org/x/crypto/chacha20poly1305", "\x10W\a\xd8\x01\x04\x01"}, + {"vendor/golang.org/x/crypto/cryptobyte", "d\n\x03\x88\x01& \n"}, + {"vendor/golang.org/x/crypto/cryptobyte/asn1", ""}, + {"vendor/golang.org/x/crypto/internal/alias", "\xc5\x02"}, + {"vendor/golang.org/x/crypto/internal/poly1305", "Q\x16\x93\x01"}, + {"vendor/golang.org/x/net/dns/dnsmessage", "n"}, + {"vendor/golang.org/x/net/http/httpguts", "\x81\x02\x14\x1b\x13\r"}, + {"vendor/golang.org/x/net/http/httpproxy", "n\x03\x90\x01\x15\x01\x19\x13\r"}, + {"vendor/golang.org/x/net/http2/hpack", "\x03k\x03zG"}, + {"vendor/golang.org/x/net/idna", "q\x87\x018\x13\x10\x02\x01"}, + {"vendor/golang.org/x/net/nettest", "\x03d\a\x03z\x11\x05\x16\x01\f\v\x01\x02\x02\x01\n"}, + {"vendor/golang.org/x/sys/cpu", "\x97\x02\r\v\x01\x15"}, + {"vendor/golang.org/x/text/secure/bidirule", "n\xd5\x01\x11\x01"}, + {"vendor/golang.org/x/text/transform", "\x03k}X"}, + {"vendor/golang.org/x/text/unicode/bidi", "\x03\bf~?\x15"}, + {"vendor/golang.org/x/text/unicode/norm", "g\nzG\x11\x11"}, + {"weak", "\x95\x01\x8f\x01!"}, +} diff --git a/internal/stdlib/deps_test.go b/internal/stdlib/deps_test.go new file mode 100644 index 00000000000..41d2d126ec5 --- /dev/null +++ b/internal/stdlib/deps_test.go @@ -0,0 +1,36 @@ +// 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 stdlib_test + +import ( + "iter" + "os" + "slices" + "sort" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/internal/stdlib" +) + +func TestImports(t *testing.T) { testDepsFunc(t, "testdata/nethttp.imports", stdlib.Imports) } +func TestDeps(t *testing.T) { testDepsFunc(t, "testdata/nethttp.deps", stdlib.Dependencies) } + +// testDepsFunc checks that the specified dependency function applied +// to net/http returns the set of dependencies in the named file. +func testDepsFunc(t *testing.T, filename string, depsFunc func(pkgs ...string) iter.Seq[string]) { + data, err := os.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + want := strings.Split(strings.TrimSpace(string(data)), "\n") + got := slices.Collect(depsFunc("net/http")) + sort.Strings(want) + sort.Strings(got) + if diff := cmp.Diff(got, want); diff != "" { + t.Fatalf("Deps mismatch (-want +got):\n%s", diff) + } +} diff --git a/internal/stdlib/generate.go b/internal/stdlib/generate.go index 1192885405c..4c67d8bd797 100644 --- a/internal/stdlib/generate.go +++ b/internal/stdlib/generate.go @@ -7,11 +7,18 @@ // The generate command reads all the GOROOT/api/go1.*.txt files and // generates a single combined manifest.go file containing the Go // standard library API symbols along with versions. +// +// It also runs "go list -deps std" and records the import graph. This +// information may be used, for example, to ensure that tools don't +// suggest fixes that import package P when analyzing one of P's +// dependencies. package main import ( "bytes" "cmp" + "encoding/binary" + "encoding/json" "errors" "fmt" "go/format" @@ -19,6 +26,7 @@ import ( "io/fs" "log" "os" + "os/exec" "path/filepath" "regexp" "runtime" @@ -29,6 +37,13 @@ import ( ) func main() { + manifest() + deps() +} + +// -- generate std manifest -- + +func manifest() { pkgs := make(map[string]map[string]symInfo) // package -> symbol -> info symRE := regexp.MustCompile(`^pkg (\S+).*?, (var|func|type|const|method \([^)]*\)) ([\pL\p{Nd}_]+)(.*)`) @@ -131,7 +146,7 @@ func main() { // Write the combined manifest. var buf bytes.Buffer - buf.WriteString(`// Copyright 2024 The Go Authors. All rights reserved. + buf.WriteString(`// 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. @@ -157,7 +172,7 @@ var PackageSymbols = map[string][]Symbol{ if err != nil { log.Fatal(err) } - if err := os.WriteFile("manifest.go", fmtbuf, 0666); err != nil { + if err := os.WriteFile("manifest.go", fmtbuf, 0o666); err != nil { log.Fatal(err) } } @@ -223,3 +238,109 @@ func removeTypeParam(s string) string { } return s } + +// -- generate dependency graph -- + +func deps() { + stdout := new(bytes.Buffer) + cmd := exec.Command("go", "list", "-deps", "-json", "std") + cmd.Stdout = stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + log.Fatal(err) + } + + type Package struct { + // go list JSON output + ImportPath string // import path of package in dir + Imports []string // import paths used by this package + + // encoding + index int + deps []int // indices of direct imports, sorted + } + pkgs := make(map[string]*Package) + var keys []string + for dec := json.NewDecoder(stdout); dec.More(); { + var pkg Package + if err := dec.Decode(&pkg); err != nil { + log.Fatal(err) + } + pkgs[pkg.ImportPath] = &pkg + keys = append(keys, pkg.ImportPath) + } + + // Sort and number the packages. + // There are 344 as of Mar 2025. + slices.Sort(keys) + for i, name := range keys { + pkgs[name].index = i + } + + // Encode the dependencies. + for _, pkg := range pkgs { + for _, imp := range pkg.Imports { + if imp == "C" { + continue + } + pkg.deps = append(pkg.deps, pkgs[imp].index) + } + slices.Sort(pkg.deps) + } + + // Emit the table. + var buf bytes.Buffer + buf.WriteString(`// 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. + +// Code generated by generate.go. DO NOT EDIT. + +package stdlib + +type pkginfo struct { + name string + deps string // list of indices of dependencies, as varint-encoded deltas +} +var deps = [...]pkginfo{ +`) + for _, name := range keys { + prev := 0 + var deps []int + for _, v := range pkgs[name].deps { + deps = append(deps, v-prev) // delta + prev = v + } + var data []byte + for _, v := range deps { + data = binary.AppendUvarint(data, uint64(v)) + } + fmt.Fprintf(&buf, "\t{%q, %q},\n", name, data) + } + fmt.Fprintln(&buf, "}") + + fmtbuf, err := format.Source(buf.Bytes()) + if err != nil { + log.Fatal(err) + } + if err := os.WriteFile("deps.go", fmtbuf, 0o666); err != nil { + log.Fatal(err) + } + + // Also generate the data for the test. + for _, t := range [...]struct{ flag, filename string }{ + {"-deps=true", "testdata/nethttp.deps"}, + {`-f={{join .Imports "\n"}}`, "testdata/nethttp.imports"}, + } { + stdout := new(bytes.Buffer) + cmd := exec.Command("go", "list", t.flag, "net/http") + cmd.Stdout = stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + log.Fatal(err) + } + if err := os.WriteFile(t.filename, stdout.Bytes(), 0666); err != nil { + log.Fatal(err) + } + } +} diff --git a/internal/stdlib/import.go b/internal/stdlib/import.go new file mode 100644 index 00000000000..f6909878a8a --- /dev/null +++ b/internal/stdlib/import.go @@ -0,0 +1,89 @@ +// 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 stdlib + +// This file provides the API for the import graph of the standard library. +// +// Be aware that the compiler-generated code for every package +// implicitly depends on package "runtime" and a handful of others +// (see runtimePkgs in GOROOT/src/cmd/internal/objabi/pkgspecial.go). + +import ( + "encoding/binary" + "iter" + "slices" + "strings" +) + +// Imports returns the sequence of packages directly imported by the +// named standard packages, in name order. +// The imports of an unknown package are the empty set. +// +// The graph is built into the application and may differ from the +// graph in the Go source tree being analyzed by the application. +func Imports(pkgs ...string) iter.Seq[string] { + return func(yield func(string) bool) { + for _, pkg := range pkgs { + if i, ok := find(pkg); ok { + var depIndex uint64 + for data := []byte(deps[i].deps); len(data) > 0; { + delta, n := binary.Uvarint(data) + depIndex += delta + if !yield(deps[depIndex].name) { + return + } + data = data[n:] + } + } + } + } +} + +// Dependencies returns the set of all dependencies of the named +// standard packages, including the initial package, +// in a deterministic topological order. +// The dependencies of an unknown package are the empty set. +// +// The graph is built into the application and may differ from the +// graph in the Go source tree being analyzed by the application. +func Dependencies(pkgs ...string) iter.Seq[string] { + return func(yield func(string) bool) { + for _, pkg := range pkgs { + if i, ok := find(pkg); ok { + var seen [1 + len(deps)/8]byte // bit set of seen packages + var visit func(i int) bool + visit = func(i int) bool { + bit := byte(1) << (i % 8) + if seen[i/8]&bit == 0 { + seen[i/8] |= bit + var depIndex uint64 + for data := []byte(deps[i].deps); len(data) > 0; { + delta, n := binary.Uvarint(data) + depIndex += delta + if !visit(int(depIndex)) { + return false + } + data = data[n:] + } + if !yield(deps[i].name) { + return false + } + } + return true + } + if !visit(i) { + return + } + } + } + } +} + +// find returns the index of pkg in the deps table. +func find(pkg string) (int, bool) { + return slices.BinarySearchFunc(deps[:], pkg, func(p pkginfo, n string) int { + return strings.Compare(p.name, n) + }) +} diff --git a/internal/stdlib/manifest.go b/internal/stdlib/manifest.go index e7d0aee2186..00776a31b60 100644 --- a/internal/stdlib/manifest.go +++ b/internal/stdlib/manifest.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Go Authors. All rights reserved. +// 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. @@ -7119,6 +7119,7 @@ var PackageSymbols = map[string][]Symbol{ {"FormatFileInfo", Func, 21}, {"Glob", Func, 16}, {"GlobFS", Type, 16}, + {"Lstat", Func, 25}, {"ModeAppend", Const, 16}, {"ModeCharDevice", Const, 16}, {"ModeDevice", Const, 16}, @@ -7143,6 +7144,8 @@ var PackageSymbols = map[string][]Symbol{ {"ReadDirFile", Type, 16}, {"ReadFile", Func, 16}, {"ReadFileFS", Type, 16}, + {"ReadLink", Func, 25}, + {"ReadLinkFS", Type, 25}, {"SkipAll", Var, 20}, {"SkipDir", Var, 16}, {"Stat", Func, 16}, @@ -9146,6 +9149,8 @@ var PackageSymbols = map[string][]Symbol{ {"(*ProcessState).SysUsage", Method, 0}, {"(*ProcessState).SystemTime", Method, 0}, {"(*ProcessState).UserTime", Method, 0}, + {"(*Root).Chmod", Method, 25}, + {"(*Root).Chown", Method, 25}, {"(*Root).Close", Method, 24}, {"(*Root).Create", Method, 24}, {"(*Root).FS", Method, 24}, @@ -16754,9 +16759,11 @@ var PackageSymbols = map[string][]Symbol{ }, "testing/fstest": { {"(MapFS).Glob", Method, 16}, + {"(MapFS).Lstat", Method, 25}, {"(MapFS).Open", Method, 16}, {"(MapFS).ReadDir", Method, 16}, {"(MapFS).ReadFile", Method, 16}, + {"(MapFS).ReadLink", Method, 25}, {"(MapFS).Stat", Method, 16}, {"(MapFS).Sub", Method, 16}, {"MapFS", Type, 16}, diff --git a/internal/stdlib/stdlib.go b/internal/stdlib/stdlib.go index 98904017f2c..3d96d3bf686 100644 --- a/internal/stdlib/stdlib.go +++ b/internal/stdlib/stdlib.go @@ -6,7 +6,7 @@ // Package stdlib provides a table of all exported symbols in the // standard library, along with the version at which they first -// appeared. +// appeared. It also provides the import graph of std packages. package stdlib import ( diff --git a/internal/stdlib/testdata/nethttp.deps b/internal/stdlib/testdata/nethttp.deps new file mode 100644 index 00000000000..e1235e84932 --- /dev/null +++ b/internal/stdlib/testdata/nethttp.deps @@ -0,0 +1,171 @@ +internal/goarch +unsafe +internal/abi +internal/unsafeheader +internal/cpu +internal/bytealg +internal/byteorder +internal/chacha8rand +internal/coverage/rtcov +internal/godebugs +internal/goexperiment +internal/goos +internal/profilerecord +internal/runtime/atomic +internal/runtime/exithook +internal/asan +internal/msan +internal/race +internal/runtime/math +internal/runtime/sys +internal/runtime/maps +internal/stringslite +internal/trace/tracev2 +runtime +internal/reflectlite +errors +sync/atomic +internal/sync +sync +io +iter +math/bits +unicode +unicode/utf8 +bytes +strings +bufio +cmp +internal/itoa +math +strconv +reflect +slices +internal/fmtsort +internal/oserror +path +internal/bisect +internal/godebug +syscall +time +io/fs +internal/filepathlite +internal/syscall/unix +internal/poll +internal/syscall/execenv +internal/testlog +os +fmt +sort +compress/flate +encoding/binary +hash +hash/crc32 +compress/gzip +container/list +context +crypto +crypto/internal/fips140deps/godebug +crypto/internal/fips140 +crypto/internal/fips140/alias +crypto/internal/fips140deps/byteorder +crypto/internal/fips140deps/cpu +crypto/internal/impl +crypto/internal/fips140/sha256 +crypto/internal/fips140/subtle +crypto/internal/fips140/sha3 +crypto/internal/fips140/sha512 +crypto/internal/fips140/hmac +crypto/internal/fips140/check +crypto/internal/fips140/aes +crypto/internal/sysrand +crypto/internal/entropy +math/rand/v2 +crypto/internal/randutil +crypto/internal/fips140/drbg +crypto/internal/fips140/aes/gcm +crypto/internal/fips140only +crypto/subtle +crypto/cipher +crypto/internal/boring/sig +crypto/internal/boring +math/rand +math/big +crypto/rand +crypto/aes +crypto/des +crypto/internal/fips140/nistec/fiat +crypto/internal/fips140/nistec +crypto/internal/fips140/ecdh +crypto/internal/fips140/edwards25519/field +crypto/ecdh +crypto/elliptic +crypto/internal/boring/bbig +crypto/internal/fips140/bigmod +crypto/internal/fips140/ecdsa +crypto/sha3 +crypto/internal/fips140hash +crypto/sha512 +unicode/utf16 +encoding/asn1 +vendor/golang.org/x/crypto/cryptobyte/asn1 +vendor/golang.org/x/crypto/cryptobyte +crypto/ecdsa +crypto/internal/fips140/edwards25519 +crypto/internal/fips140/ed25519 +crypto/ed25519 +crypto/hmac +crypto/internal/fips140/hkdf +crypto/internal/fips140/mlkem +crypto/internal/fips140/tls12 +crypto/internal/fips140/tls13 +vendor/golang.org/x/crypto/internal/alias +vendor/golang.org/x/crypto/chacha20 +vendor/golang.org/x/crypto/internal/poly1305 +vendor/golang.org/x/crypto/chacha20poly1305 +crypto/internal/hpke +crypto/md5 +crypto/rc4 +crypto/internal/fips140/rsa +crypto/rsa +crypto/sha1 +crypto/sha256 +crypto/tls/internal/fips140tls +crypto/dsa +crypto/x509/internal/macos +encoding/hex +crypto/x509/pkix +encoding/base64 +encoding/pem +maps +vendor/golang.org/x/net/dns/dnsmessage +internal/nettrace +weak +unique +net/netip +internal/routebsd +internal/singleflight +net +net/url +crypto/x509 +crypto/tls +vendor/golang.org/x/text/transform +log/internal +log +vendor/golang.org/x/text/unicode/bidi +vendor/golang.org/x/text/secure/bidirule +vendor/golang.org/x/text/unicode/norm +vendor/golang.org/x/net/idna +net/textproto +vendor/golang.org/x/net/http/httpguts +vendor/golang.org/x/net/http/httpproxy +vendor/golang.org/x/net/http2/hpack +mime +mime/quotedprintable +path/filepath +mime/multipart +net/http/httptrace +net/http/internal +net/http/internal/ascii +net/http/internal/httpcommon +net/http diff --git a/internal/stdlib/testdata/nethttp.imports b/internal/stdlib/testdata/nethttp.imports new file mode 100644 index 00000000000..77e78696bdd --- /dev/null +++ b/internal/stdlib/testdata/nethttp.imports @@ -0,0 +1,47 @@ +bufio +bytes +compress/gzip +container/list +context +crypto/rand +crypto/tls +encoding/base64 +encoding/binary +errors +fmt +vendor/golang.org/x/net/http/httpguts +vendor/golang.org/x/net/http/httpproxy +vendor/golang.org/x/net/http2/hpack +vendor/golang.org/x/net/idna +internal/godebug +io +io/fs +log +maps +math +math/bits +math/rand +mime +mime/multipart +net +net/http/httptrace +net/http/internal +net/http/internal/ascii +net/http/internal/httpcommon +net/textproto +net/url +os +path +path/filepath +reflect +runtime +slices +sort +strconv +strings +sync +sync/atomic +time +unicode +unicode/utf8 +unsafe From 5f02a3e879c4c45e42f3a630f971a0f6a13110e5 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 28 Feb 2025 10:31:17 -0500 Subject: [PATCH 85/99] gopls/internal/analysis/modernize: don't import slices within slices This CL strengthens the check that the various modernizer passes use to skip packages in which the fixes cannot be applied. Before, we would not add an import of "slices" from withing the "slices" package itself, but we cannot add this import from any package that "slices" itself transitively depends upon, as this would create an import cycle. So, we consult the std dependency graph baked into the executable. This feature was tested interactively by running modernize on std. Updates golang/go#71847 Change-Id: Iaec6ef07b58ca07df498db63369dae8087331ab9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/653595 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- gopls/internal/analysis/modernize/maps.go | 4 +++- .../internal/analysis/modernize/modernize.go | 22 +++++++++++++++++++ gopls/internal/analysis/modernize/slices.go | 5 +++-- .../analysis/modernize/slicescontains.go | 5 +++-- .../analysis/modernize/slicesdelete.go | 6 +++++ .../internal/analysis/modernize/sortslice.go | 4 +++- gopls/internal/util/moreiters/iters.go | 10 +++++++++ 7 files changed, 50 insertions(+), 6 deletions(-) diff --git a/gopls/internal/analysis/modernize/maps.go b/gopls/internal/analysis/modernize/maps.go index dad329477cd..5577978278c 100644 --- a/gopls/internal/analysis/modernize/maps.go +++ b/gopls/internal/analysis/modernize/maps.go @@ -41,7 +41,9 @@ import ( // m = make(M) // m = M{} func mapsloop(pass *analysis.Pass) { - if pass.Pkg.Path() == "maps " { + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "maps", "bytes", "runtime") { return } diff --git a/gopls/internal/analysis/modernize/modernize.go b/gopls/internal/analysis/modernize/modernize.go index 0f7b58eed37..354836d6b40 100644 --- a/gopls/internal/analysis/modernize/modernize.go +++ b/gopls/internal/analysis/modernize/modernize.go @@ -18,8 +18,10 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/util/astutil" + "golang.org/x/tools/gopls/internal/util/moreiters" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/astutil/cursor" + "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/versions" ) @@ -125,6 +127,26 @@ func filesUsing(inspect *inspector.Inspector, info *types.Info, version string) } } +// within reports whether the current pass is analyzing one of the +// specified standard packages or their dependencies. +func within(pass *analysis.Pass, pkgs ...string) bool { + path := pass.Pkg.Path() + return standard(path) && + moreiters.Contains(stdlib.Dependencies(pkgs...), path) +} + +// standard reports whether the specified package path belongs to a +// package in the standard library (including internal dependencies). +func standard(path string) bool { + // A standard package has no dot in its first segment. + // (It may yet have a dot, e.g. "vendor/golang.org/x/foo".) + slash := strings.IndexByte(path, '/') + if slash < 0 { + slash = len(path) + } + return !strings.Contains(path[:slash], ".") && path != "testdata" +} + var ( builtinAny = types.Universe.Lookup("any") builtinAppend = types.Universe.Lookup("append") diff --git a/gopls/internal/analysis/modernize/slices.go b/gopls/internal/analysis/modernize/slices.go index 9cca3e98156..7e0d9cbd92e 100644 --- a/gopls/internal/analysis/modernize/slices.go +++ b/gopls/internal/analysis/modernize/slices.go @@ -46,8 +46,9 @@ import ( // The fix does not always preserve nilness the of base slice when the // addends (a, b, c) are all empty. func appendclipped(pass *analysis.Pass) { - switch pass.Pkg.Path() { - case "slices", "bytes": + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "slices", "bytes", "runtime") { return } diff --git a/gopls/internal/analysis/modernize/slicescontains.go b/gopls/internal/analysis/modernize/slicescontains.go index 09642448bb5..b59ea452a0f 100644 --- a/gopls/internal/analysis/modernize/slicescontains.go +++ b/gopls/internal/analysis/modernize/slicescontains.go @@ -47,8 +47,9 @@ import ( // (Mostly this appears to be a desirable optimization, avoiding // redundantly repeated evaluation.) func slicescontains(pass *analysis.Pass) { - // Don't modify the slices package itself. - if pass.Pkg.Path() == "slices" { + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "slices", "runtime") { return } diff --git a/gopls/internal/analysis/modernize/slicesdelete.go b/gopls/internal/analysis/modernize/slicesdelete.go index 24b2182ca6a..3c3d880f62b 100644 --- a/gopls/internal/analysis/modernize/slicesdelete.go +++ b/gopls/internal/analysis/modernize/slicesdelete.go @@ -21,6 +21,12 @@ import ( // Other variations that will also have suggested replacements include: // append(s[:i-1], s[i:]...) and append(s[:i+k1], s[i+k2:]) where k2 > k1. func slicesdelete(pass *analysis.Pass) { + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "slices", "runtime") { + return + } + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) info := pass.TypesInfo report := func(file *ast.File, call *ast.CallExpr, slice1, slice2 *ast.SliceExpr) { diff --git a/gopls/internal/analysis/modernize/sortslice.go b/gopls/internal/analysis/modernize/sortslice.go index a033be7f635..0437aaf2f67 100644 --- a/gopls/internal/analysis/modernize/sortslice.go +++ b/gopls/internal/analysis/modernize/sortslice.go @@ -36,7 +36,9 @@ import ( // - sort.Sort(x) where x has a named slice type whose Less method is the natural order. // -> sort.Slice(x) func sortslice(pass *analysis.Pass) { - if !analysisinternal.Imports(pass.Pkg, "sort") { + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "slices", "sort", "runtime") { return } diff --git a/gopls/internal/util/moreiters/iters.go b/gopls/internal/util/moreiters/iters.go index e4d83ae8618..d41cb1d3bca 100644 --- a/gopls/internal/util/moreiters/iters.go +++ b/gopls/internal/util/moreiters/iters.go @@ -14,3 +14,13 @@ func First[T any](seq iter.Seq[T]) (z T, ok bool) { } return z, false } + +// Contains reports whether x is an element of the sequence seq. +func Contains[T comparable](seq iter.Seq[T], x T) bool { + for cand := range seq { + if cand == x { + return true + } + } + return false +} From d14149970b9aba669e3257bfc34df2994a2a2fbc Mon Sep 17 00:00:00 2001 From: Egon Elbre Date: Thu, 20 Feb 2025 13:43:48 +0200 Subject: [PATCH 86/99] cmd/toolstash: fix windows executable name handling Change-Id: I1ff643fae4c48b4f68b452eb6881fca99832930c Reviewed-on: https://go-review.googlesource.com/c/tools/+/650915 Reviewed-by: Junyang Shao Reviewed-by: Michael Pratt Auto-Submit: Sean Liao Commit-Queue: Sean Liao Reviewed-by: Sean Liao LUCI-TryBot-Result: Go LUCI --- cmd/toolstash/main.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cmd/toolstash/main.go b/cmd/toolstash/main.go index c533ed1e572..3a92c00bfff 100644 --- a/cmd/toolstash/main.go +++ b/cmd/toolstash/main.go @@ -225,7 +225,7 @@ func main() { return } - tool = cmd[0] + tool = exeName(cmd[0]) if i := strings.LastIndexAny(tool, `/\`); i >= 0 { tool = tool[i+1:] } @@ -530,7 +530,7 @@ func runCmd(cmd []string, keepLog bool, logName string) (output []byte, err erro }() } - xcmd := exec.Command(cmd[0], cmd[1:]...) + xcmd := exec.Command(exeName(cmd[0]), cmd[1:]...) if !keepLog { return xcmd.CombinedOutput() } @@ -571,9 +571,10 @@ func save() { if !shouldSave(name) { continue } - src := filepath.Join(binDir, name) + bin := exeName(name) + src := filepath.Join(binDir, bin) if _, err := os.Stat(src); err == nil { - cp(src, filepath.Join(stashDir, name)) + cp(src, filepath.Join(stashDir, bin)) } } @@ -641,3 +642,10 @@ func cp(src, dst string) { log.Fatal(err) } } + +func exeName(name string) string { + if runtime.GOOS == "windows" { + return name + ".exe" + } + return name +} From 0efa5e51a822f9f580ed226cd8cd96089bc2d80d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 28 Feb 2025 15:56:52 -0500 Subject: [PATCH 87/99] gopls/internal/analysis/modernize: rangeint: non-integer untyped constants This CL fixes a bug in rangeint that caused it to replace const limit = 1e3 for i := 0; i < limit; i++ {} with for range limit {} // error: limit is not an integer Now, we check that the type of limit is assignable to int, and if not insert an explicit int(limit) conversion. Updates golang/go#71847 (item d) Change-Id: Icfaa96e5506fcb5a3e6f3ed8f911bf4bda9cf32f Reviewed-on: https://go-review.googlesource.com/c/tools/+/653616 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam Auto-Submit: Alan Donovan --- gopls/internal/analysis/modernize/rangeint.go | 42 +++++++++++++++++++ .../testdata/src/rangeint/rangeint.go | 11 +++++ .../testdata/src/rangeint/rangeint.go.golden | 11 +++++ 3 files changed, 64 insertions(+) diff --git a/gopls/internal/analysis/modernize/rangeint.go b/gopls/internal/analysis/modernize/rangeint.go index b94bff34431..2921bbb3468 100644 --- a/gopls/internal/analysis/modernize/rangeint.go +++ b/gopls/internal/analysis/modernize/rangeint.go @@ -31,6 +31,8 @@ import ( // - The ':=' may be replaced by '='. // - The fix may remove "i :=" if it would become unused. // +// TODO(adonovan): permit variants such as "i := int64(0)". +// // Restrictions: // - The variable i must not be assigned or address-taken within the // loop, because a "for range int" loop does not respect assignments @@ -120,6 +122,31 @@ func rangeint(pass *analysis.Pass) { limit = call.Args[0] } + // If the limit is a untyped constant of non-integer type, + // such as "const limit = 1e3", its effective type may + // differ between the two forms. + // In a for loop, it must be comparable with int i, + // for i := 0; i < limit; i++ + // but in a range loop it would become a float, + // for i := range limit {} + // which is a type error. We need to convert it to int + // in this case. + // + // Unfortunately go/types discards the untyped type + // (but see Untyped in golang/go#70638) so we must + // re-type check the expression to detect this case. + var beforeLimit, afterLimit string + if v := info.Types[limit].Value; v != nil { + beforeLimit, afterLimit = "int(", ")" + info2 := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)} + if types.CheckExpr(pass.Fset, pass.Pkg, limit.Pos(), limit, info2) == nil { + tLimit := types.Default(info2.TypeOf(limit)) + if types.AssignableTo(tLimit, types.Typ[types.Int]) { + beforeLimit, afterLimit = "", "" + } + } + } + pass.Report(analysis.Diagnostic{ Pos: init.Pos(), End: inc.End(), @@ -133,15 +160,30 @@ func rangeint(pass *analysis.Pass) { // ----- --- // ------- // for i := range limit {} + + // Delete init. { Pos: init.Rhs[0].Pos(), End: limit.Pos(), NewText: []byte("range "), }, + // Add "int(" before limit, if needed. + { + Pos: limit.Pos(), + End: limit.Pos(), + NewText: []byte(beforeLimit), + }, + // Delete inc. { Pos: limit.End(), End: inc.End(), }, + // Add ")" after limit, if needed. + { + Pos: limit.End(), + End: limit.End(), + NewText: []byte(afterLimit), + }, }...), }}, }) diff --git a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go index 32628f5fae3..da486dcd32c 100644 --- a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go +++ b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go @@ -66,3 +66,14 @@ func nopePostconditionDiffers() { } println(i) // must print 5, not 4 } + +// Non-integer untyped constants need to be explicitly converted to int. +func issue71847d() { + const limit = 1e3 // float + for i := 0; i < limit; i++ { // want "for loop can be modernized using range over int" + } + + const limit2 = 1 + 0i // complex + for i := 0; i < limit2; i++ { // want "for loop can be modernized using range over int" + } +} diff --git a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden index 43cf220d699..01d28ccb92b 100644 --- a/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden +++ b/gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden @@ -66,3 +66,14 @@ func nopePostconditionDiffers() { } println(i) // must print 5, not 4 } + +// Non-integer untyped constants need to be explicitly converted to int. +func issue71847d() { + const limit = 1e3 // float + for range int(limit) { // want "for loop can be modernized using range over int" + } + + const limit2 = 1 + 0i // complex + for range int(limit2) { // want "for loop can be modernized using range over int" + } +} From 2839096cd63a762fc544b0a489afe080032c472d Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Fri, 21 Feb 2025 15:03:43 -0500 Subject: [PATCH 88/99] gopls/internal/analysis/gofix: generic aliases Support inlining generic aliases. For golang/go#32816. Change-Id: Ic65e6fb30d65ee0f7d6e0093fd882a675de71da4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651617 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/analysis/gofix/gofix.go | 68 +++++++++++++------ .../analysis/gofix/testdata/src/a/a.go | 22 ++++++ .../analysis/gofix/testdata/src/a/a.go.golden | 25 +++++++ 3 files changed, 96 insertions(+), 19 deletions(-) diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index 7323028aa31..7a55d7ca93d 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -9,6 +9,7 @@ import ( "go/ast" "go/token" "go/types" + "slices" "strings" _ "embed" @@ -140,11 +141,6 @@ func (a *analyzer) findAlias(spec *ast.TypeSpec, declInline bool) { } } - if spec.TypeParams != nil { - // TODO(jba): handle generic aliases - return - } - // Remember that this is an inlinable alias. typ := &goFixInlineAliasFact{} lhs := a.pass.TypesInfo.Defs[spec.Name].(*types.TypeName) @@ -294,7 +290,7 @@ func (a *analyzer) inlineCall(call *ast.CallExpr, cur cursor.Cursor) { } // If tn is the TypeName of an inlinable alias, suggest inlining its use at cur. -func (a *analyzer) inlineAlias(tn *types.TypeName, cur cursor.Cursor) { +func (a *analyzer) inlineAlias(tn *types.TypeName, curId cursor.Cursor) { inalias, ok := a.inlinableAliases[tn] if !ok { var fact goFixInlineAliasFact @@ -307,12 +303,17 @@ func (a *analyzer) inlineAlias(tn *types.TypeName, cur cursor.Cursor) { return // nope } - // Get the alias's RHS. It has everything we need to format the replacement text. - rhs := tn.Type().(*types.Alias).Rhs() - + alias := tn.Type().(*types.Alias) + // Remember the names of the alias's type params. When we check for shadowing + // later, we'll ignore these because they won't appear in the replacement text. + typeParamNames := map[*types.TypeName]bool{} + for tp := range alias.TypeParams().TypeParams() { + typeParamNames[tp.Obj()] = true + } + rhs := alias.Rhs() curPath := a.pass.Pkg.Path() - curFile := currentFile(cur) - n := cur.Node().(*ast.Ident) + curFile := currentFile(curId) + id := curId.Node().(*ast.Ident) // We have an identifier A here (n), possibly qualified by a package // identifier (sel.n), and an inlinable "type A = rhs" elsewhere. // @@ -324,6 +325,10 @@ func (a *analyzer) inlineAlias(tn *types.TypeName, cur cursor.Cursor) { edits []analysis.TextEdit ) for _, tn := range typenames(rhs) { + // Ignore the type parameters of the alias: they won't appear in the result. + if typeParamNames[tn] { + continue + } var pkgPath, pkgName string if pkg := tn.Pkg(); pkg != nil { pkgPath = pkg.Path() @@ -333,9 +338,9 @@ func (a *analyzer) inlineAlias(tn *types.TypeName, cur cursor.Cursor) { // The name is in the current package or the universe scope, so no import // is required. Check that it is not shadowed (that is, that the type // it refers to in rhs is the same one it refers to at n). - scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope - _, obj := scope.LookupParent(tn.Name(), n.Pos()) // what qn.name means in n's scope - if obj != tn { // shadowed + scope := a.pass.TypesInfo.Scopes[curFile].Innermost(id.Pos()) // n's scope + _, obj := scope.LookupParent(tn.Name(), id.Pos()) // what qn.name means in n's scope + if obj != tn { return } } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), pkgPath) { @@ -345,15 +350,40 @@ func (a *analyzer) inlineAlias(tn *types.TypeName, cur cursor.Cursor) { // Use AddImport to add pkgPath if it's not there already. Associate the prefix it assigns // with the package path for use by the TypeString qualifier below. _, prefix, eds := analysisinternal.AddImport( - a.pass.TypesInfo, curFile, pkgName, pkgPath, tn.Name(), n.Pos()) + a.pass.TypesInfo, curFile, pkgName, pkgPath, tn.Name(), id.Pos()) importPrefixes[pkgPath] = strings.TrimSuffix(prefix, ".") edits = append(edits, eds...) } } - // If n is qualified by a package identifier, we'll need the full selector expression. - var expr ast.Expr = n - if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { - expr = cur.Parent().Node().(ast.Expr) + // Find the complete identifier, which may take any of these forms: + // Id + // Id[T] + // Id[K, V] + // pkg.Id + // pkg.Id[T] + // pkg.Id[K, V] + var expr ast.Expr = id + if e, _ := curId.Edge(); e == edge.SelectorExpr_Sel { + curId = curId.Parent() + expr = curId.Node().(ast.Expr) + } + // If expr is part of an IndexExpr or IndexListExpr, we'll need that node. + // Given C[int], TypeOf(C) is generic but TypeOf(C[int]) is instantiated. + switch ek, _ := curId.Edge(); ek { + case edge.IndexExpr_X: + expr = curId.Parent().Node().(*ast.IndexExpr) + case edge.IndexListExpr_X: + expr = curId.Parent().Node().(*ast.IndexListExpr) + } + t := a.pass.TypesInfo.TypeOf(expr).(*types.Alias) // type of entire identifier + if targs := t.TypeArgs(); targs.Len() > 0 { + // Instantiate the alias with the type args from this use. + // For example, given type A = M[K, V], compute the type of the use + // A[int, Foo] as M[int, Foo]. + // Don't validate instantiation: it can't panic unless we have a bug, + // in which case seeing the stack trace via telemetry would be helpful. + instAlias, _ := types.Instantiate(nil, alias, slices.Collect(targs.Types()), false) + rhs = instAlias.(*types.Alias).Rhs() } // To get the replacement text, render the alias RHS using the package prefixes // we assigned above. diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go b/gopls/internal/analysis/gofix/testdata/src/a/a.go index 49a0587c2b1..60a55052584 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/a.go +++ b/gopls/internal/analysis/gofix/testdata/src/a/a.go @@ -164,3 +164,25 @@ func _[P any]() { _ = x _ = y } + +// generic type aliases + +//go:fix inline +type ( + Mapset[T comparable] = map[T]bool // want Mapset: `goFixInline alias` + Pair[X, Y any] = struct { // want Pair: `goFixInline alias` + X X + Y Y + } +) + +var _ Mapset[int] // want `Type alias Mapset\[int\] should be inlined` + +var _ Pair[T, string] // want `Type alias Pair\[T, string\] should be inlined` + +func _[V any]() { + //go:fix inline + type M[K comparable] = map[K]V + + var _ M[int] // want `Type alias M\[int\] should be inlined` +} diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden index 9d4c527919e..c637da103ee 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden +++ b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden @@ -165,3 +165,28 @@ func _[P any]() { _ = x _ = y } + +// generic type aliases + +//go:fix inline +type ( + Mapset[T comparable] = map[T]bool // want Mapset: `goFixInline alias` + Pair[X, Y any] = struct { // want Pair: `goFixInline alias` + X X + Y Y + } +) + +var _ map[int]bool // want `Type alias Mapset\[int\] should be inlined` + +var _ struct { + X T + Y string +} // want `Type alias Pair\[T, string\] should be inlined` + +func _[V any]() { + //go:fix inline + type M[K comparable] = map[K]V + + var _ map[int]V // want `Type alias M\[int\] should be inlined` +} From 0ffdb82ead2753b9fbba8bf0932ba396b13ba6ea Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Fri, 21 Feb 2025 16:55:53 -0500 Subject: [PATCH 89/99] gopls/internal/analysis/gofix: add vet analyzer Add a second analyzer that checks for valid go:fix inline directives without suggesting changes. Change-Id: I0b9ad3da79f554caef01dda66ef954c59718015d Reviewed-on: https://go-review.googlesource.com/c/tools/+/651656 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/analysis/gofix/doc.go | 10 ++- gopls/internal/analysis/gofix/gofix.go | 26 ++++++- gopls/internal/analysis/gofix/gofix_test.go | 5 ++ .../gofix/testdata/src/directive/directive.go | 63 ++++++++++++++++ .../src/directive/directive.go.golden | 71 +++++++++++++++++++ 5 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 gopls/internal/analysis/gofix/testdata/src/directive/directive.go create mode 100644 gopls/internal/analysis/gofix/testdata/src/directive/directive.go.golden diff --git a/gopls/internal/analysis/gofix/doc.go b/gopls/internal/analysis/gofix/doc.go index ad8b067daa4..15de4f28b27 100644 --- a/gopls/internal/analysis/gofix/doc.go +++ b/gopls/internal/analysis/gofix/doc.go @@ -5,7 +5,8 @@ /* Package gofix defines an Analyzer that inlines calls to functions and uses of constants -marked with a "//go:fix inline" doc comment. +marked with a "//go:fix inline" directive. +A second analyzer only checks uses of the directive. # Analyzer gofix @@ -81,5 +82,12 @@ The proposal https://go.dev/issue/32816 introduces the "//go:fix" directives. You can use this (officially unsupported) command to apply gofix fixes en masse: $ go run golang.org/x/tools/gopls/internal/analysis/gofix/cmd/gofix@latest -test ./... + +# Analyzer gofixdirective + +gofixdirective: validate uses of gofix comment directives + +The gofixdirective analyzer checks "//go:fix inline" directives for correctness. +See the documentation for the gofix analyzer for more about "/go:fix inline". */ package gofix diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index 7a55d7ca93d..df7154ca2fc 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -34,7 +34,20 @@ var Analyzer = &analysis.Analyzer{ Name: "gofix", Doc: analysisinternal.MustExtractDoc(doc, "gofix"), URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/gofix", - Run: run, + Run: func(pass *analysis.Pass) (any, error) { return run(pass, true) }, + FactTypes: []analysis.Fact{ + (*goFixInlineFuncFact)(nil), + (*goFixInlineConstFact)(nil), + (*goFixInlineAliasFact)(nil), + }, + Requires: []*analysis.Analyzer{inspect.Analyzer}, +} + +var DirectiveAnalyzer = &analysis.Analyzer{ + Name: "gofixdirective", + Doc: analysisinternal.MustExtractDoc(doc, "gofixdirective"), + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/gofix", + Run: func(pass *analysis.Pass) (any, error) { return run(pass, false) }, FactTypes: []analysis.Fact{ (*goFixInlineFuncFact)(nil), (*goFixInlineConstFact)(nil), @@ -46,6 +59,7 @@ var Analyzer = &analysis.Analyzer{ // analyzer holds the state for this analysis. type analyzer struct { pass *analysis.Pass + fix bool // only suggest fixes if true; else, just check directives root cursor.Cursor // memoization of repeated calls for same file. fileContent map[string][]byte @@ -55,9 +69,10 @@ type analyzer struct { inlinableAliases map[*types.TypeName]*goFixInlineAliasFact } -func run(pass *analysis.Pass) (any, error) { +func run(pass *analysis.Pass, fix bool) (any, error) { a := &analyzer{ pass: pass, + fix: fix, root: cursor.Root(pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)), fileContent: make(map[string][]byte), inlinableFuncs: make(map[*types.Func]*inline.Callee), @@ -256,6 +271,10 @@ func (a *analyzer) inlineCall(call *ast.CallExpr, cur cursor.Cursor) { a.pass.Reportf(call.Lparen, "%v", err) return } + if !a.fix { + return + } + if res.Literalized { // Users are not fond of inlinings that literalize // f(x) to func() { ... }(), so avoid them. @@ -533,6 +552,9 @@ func (a *analyzer) inlineConst(con *types.Const, cur cursor.Cursor) { // reportInline reports a diagnostic for fixing an inlinable name. func (a *analyzer) reportInline(kind, capKind string, ident ast.Expr, edits []analysis.TextEdit, newText string) { + if !a.fix { + return + } edits = append(edits, analysis.TextEdit{ Pos: ident.Pos(), End: ident.End(), diff --git a/gopls/internal/analysis/gofix/gofix_test.go b/gopls/internal/analysis/gofix/gofix_test.go index dc98ef47181..4acc4daf2ff 100644 --- a/gopls/internal/analysis/gofix/gofix_test.go +++ b/gopls/internal/analysis/gofix/gofix_test.go @@ -22,6 +22,11 @@ func TestAnalyzer(t *testing.T) { analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), Analyzer, "a", "b") } +func TestDirectiveAnalyzer(t *testing.T) { + analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), DirectiveAnalyzer, "directive") + +} + func TestTypesWithNames(t *testing.T) { // Test setup inspired by internal/analysisinternal/addimport_test.go. testenv.NeedsDefaultImporter(t) diff --git a/gopls/internal/analysis/gofix/testdata/src/directive/directive.go b/gopls/internal/analysis/gofix/testdata/src/directive/directive.go new file mode 100644 index 00000000000..47c2884c386 --- /dev/null +++ b/gopls/internal/analysis/gofix/testdata/src/directive/directive.go @@ -0,0 +1,63 @@ +package directive + +// Functions. + +func f() { + One() + + new(T).Two() +} + +type T struct{} + +//go:fix inline +func One() int { return one } // want One:`goFixInline directive.One` + +const one = 1 + +//go:fix inline +func (T) Two() int { return 2 } // want Two:`goFixInline \(directive.T\).Two` + +// Constants. + +const Uno = 1 + +//go:fix inline +const In1 = Uno // want In1: `goFixInline const "directive".Uno` + +const ( + no1 = one + + //go:fix inline + In2 = one // want In2: `goFixInline const "directive".one` +) + +//go:fix inline +const bad1 = 1 // want `invalid //go:fix inline directive: const value is not the name of another constant` + +//go:fix inline +const in5, + in6, + bad2 = one, one, + one + 1 // want `invalid //go:fix inline directive: const value is not the name of another constant` + +// Make sure we don't crash on iota consts, but still process the whole decl. +// +//go:fix inline +const ( + a = iota // want `invalid //go:fix inline directive: const value is iota` + b + in7 = one +) + +const ( + x = 1 + //go:fix inline + in8 = x +) + +//go:fix inline +const in9 = iota // want `invalid //go:fix inline directive: const value is iota` + +//go:fix inline +type E = map[[Uno]string][]*T // want `invalid //go:fix inline directive: array types not supported` diff --git a/gopls/internal/analysis/gofix/testdata/src/directive/directive.go.golden b/gopls/internal/analysis/gofix/testdata/src/directive/directive.go.golden new file mode 100644 index 00000000000..3e5b3409288 --- /dev/null +++ b/gopls/internal/analysis/gofix/testdata/src/directive/directive.go.golden @@ -0,0 +1,71 @@ +package golden + +import "a/internal" + +// Functions. + +func f() { + One() + + new(T).Two() +} + +type T struct{} + +//go:fix inline +func One() int { return one } + +const one = 1 + +//go:fix inline +func (T) Two() int { return 2 } + +// Constants. + +const Uno = 1 + +//go:fix inline +const In1 = Uno // want In1: `goFixInline const "a".Uno` + +const ( + no1 = one + + //go:fix inline + In2 = one // want In2: `goFixInline const "a".one` +) + +//go:fix inline +const bad1 = 1 // want `invalid //go:fix inline directive: const value is not the name of another constant` + +//go:fix inline +const in5, + in6, + bad2 = one, one, + one + 1 // want `invalid //go:fix inline directive: const value is not the name of another constant` + +// Make sure we don't crash on iota consts, but still process the whole decl. +// +//go:fix inline +const ( + a = iota // want `invalid //go:fix inline directive: const value is iota` + b + in7 = one +) + +const ( + x = 1 + //go:fix inline + in8 = x +) + +//go:fix inline +const a = iota // want `invalid //go:fix inline directive: const value is iota` + +//go:fix inline +type E = map[[Uno]string][]*T // want `invalid //go:fix inline directive: array types not supported` + +// literal array lengths are OK +// +//go:fix inline +type EL = map[[2]string][]*T // want EL: `goFixInline alias` + From 2b1f55036370bc9a05bed74aa13fa85fecce40e2 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Fri, 21 Feb 2025 16:24:42 -0500 Subject: [PATCH 90/99] gopls/internal/analysis/gofix: allow literal array lengths An array type can be inlined if its length is a literal integer. For golang/go#32816. Change-Id: I80c7f18721c813a0ea7039411ddf8a804b5bf0b5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/651655 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/analysis/gofix/gofix.go | 5 ++++- gopls/internal/analysis/gofix/testdata/src/a/a.go | 7 +++++++ gopls/internal/analysis/gofix/testdata/src/a/a.go.golden | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index df7154ca2fc..41cebcb63b9 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -148,9 +148,12 @@ func (a *analyzer) findAlias(spec *ast.TypeSpec, declInline bool) { // type A = [N]int // // would result in [5]int, breaking the connection with N. - // TODO(jba): accept type expressions where the array size is a literal integer for n := range ast.Preorder(spec.Type) { if ar, ok := n.(*ast.ArrayType); ok && ar.Len != nil { + // Make an exception when the array length is a literal int. + if lit, ok := ast.Unparen(ar.Len).(*ast.BasicLit); ok && lit.Kind == token.INT { + continue + } a.pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: array types not supported") return } diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go b/gopls/internal/analysis/gofix/testdata/src/a/a.go index 60a55052584..96f4f4d4e13 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/a.go +++ b/gopls/internal/analysis/gofix/testdata/src/a/a.go @@ -129,6 +129,13 @@ type E = map[[Uno]string][]*T // want `invalid //go:fix inline directive: array var _ E // nothing should happen here +// literal array lengths are OK +// +//go:fix inline +type EL = map[[2]string][]*T // want EL: `goFixInline alias` + +var _ EL // want `Type alias EL should be inlined` + //go:fix inline type F = map[internal.T]T // want F: `goFixInline alias` diff --git a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden index c637da103ee..64d08ec1548 100644 --- a/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden +++ b/gopls/internal/analysis/gofix/testdata/src/a/a.go.golden @@ -129,6 +129,13 @@ type E = map[[Uno]string][]*T // want `invalid //go:fix inline directive: array var _ E // nothing should happen here +// literal array lengths are OK +// +//go:fix inline +type EL = map[[2]string][]*T // want EL: `goFixInline alias` + +var _ map[[2]string][]*T // want `Type alias EL should be inlined` + //go:fix inline type F = map[internal.T]T // want F: `goFixInline alias` From 455db21bd963fea3efdf0473e0ddce37313b8f91 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 28 Feb 2025 11:09:28 -0500 Subject: [PATCH 91/99] gopls/internal/cache/parsego: fix OOB crash in fixInitStmt This is a priori not how to use safetoken. I haven't attempted to reproduce the crash. Fixes golang/go#72026 Change-Id: I7a95383032f9a882c8b667203bbe0cf06f85a987 Reviewed-on: https://go-review.googlesource.com/c/tools/+/653596 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- gopls/internal/cache/parsego/parse.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go index 08a1c395a2a..fd598e235d1 100644 --- a/gopls/internal/cache/parsego/parse.go +++ b/gopls/internal/cache/parsego/parse.go @@ -528,11 +528,11 @@ func fixInitStmt(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) } // Try to extract a statement from the BadExpr. - start, end, err := safetoken.Offsets(tok, bad.Pos(), bad.End()-1) + start, end, err := safetoken.Offsets(tok, bad.Pos(), bad.End()) if err != nil { return false } - stmtBytes := src[start : end+1] + stmtBytes := src[start:end] stmt, err := parseStmt(tok, bad.Pos(), stmtBytes) if err != nil { return false From d81d6fcce1a24f2b8d0a9493f4d84b75c80176e4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 28 Feb 2025 18:40:18 -0500 Subject: [PATCH 92/99] gopls/internal/util/asm: better assembly parsing This CL adds a rudimentary parser for symbols in Go .s files. It is a placeholder for a more principled implementation, but it is sufficient to make Definition support control labels (also in this CL) and for a cross-references index (future work). + test of Definition on control label + test of asm.Parse Updates golang/go#71754 Change-Id: I2ff19b4ade130c051197d6b097a1a3dbcd95555a Reviewed-on: https://go-review.googlesource.com/c/tools/+/654335 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam Auto-Submit: Alan Donovan --- gopls/internal/goasm/definition.go | 59 +++-- gopls/internal/golang/assembly.go | 3 + .../test/marker/testdata/definition/asm.txt | 3 + gopls/internal/util/asm/parse.go | 245 ++++++++++++++++++ gopls/internal/util/asm/parse_test.go | 67 +++++ 5 files changed, 353 insertions(+), 24 deletions(-) create mode 100644 gopls/internal/util/asm/parse.go create mode 100644 gopls/internal/util/asm/parse_test.go diff --git a/gopls/internal/goasm/definition.go b/gopls/internal/goasm/definition.go index 4403e7cac7f..903916d265d 100644 --- a/gopls/internal/goasm/definition.go +++ b/gopls/internal/goasm/definition.go @@ -2,20 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package goasm provides language-server features for files in Go +// assembly language (https://go.dev/doc/asm). package goasm import ( - "bytes" "context" "fmt" "go/token" - "strings" - "unicode" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/asm" "golang.org/x/tools/gopls/internal/util/morestrings" "golang.org/x/tools/internal/event" ) @@ -41,21 +41,27 @@ func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p return nil, err } + // Parse the assembly. + // + // TODO(adonovan): make this just another + // attribute of the type-checked cache.Package. + file := asm.Parse(content) + // Figure out the selected symbol. // For now, just find the identifier around the cursor. - // - // TODO(adonovan): use a real asm parser; see cmd/asm/internal/asm/parse.go. - // Ideally this would just be just another attribute of the - // type-checked cache.Package. - nonIdentRune := func(r rune) bool { return !isIdentRune(r) } - i := bytes.LastIndexFunc(content[:offset], nonIdentRune) - j := bytes.IndexFunc(content[offset:], nonIdentRune) - if j < 0 || j == 0 { - return nil, nil // identifier runs to EOF, or not an identifier + var found *asm.Ident + for _, id := range file.Idents { + if id.Offset <= offset && offset <= id.End() { + found = &id + break + } } - sym := string(content[i+1 : offset+j]) - sym = strings.ReplaceAll(sym, "·", ".") // (U+00B7 MIDDLE DOT) - sym = strings.ReplaceAll(sym, "∕", "/") // (U+2215 DIVISION SLASH) + if found == nil { + return nil, fmt.Errorf("not an identifier") + } + + // Resolve a symbol with a "." prefix to the current package. + sym := found.Name if sym != "" && sym[0] == '.' { sym = string(mp.PkgPath) + sym } @@ -92,18 +98,23 @@ func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p if err == nil { return []protocol.Location{loc}, nil } - } - // TODO(adonovan): support jump to var, block label, and other - // TEXT, DATA, and GLOBAL symbols in the same file. Needs asm parser. + } else { + // local symbols (funcs, vars, labels) + for _, id := range file.Idents { + if id.Name == found.Name && + (id.Kind == asm.Text || id.Kind == asm.Global || id.Kind == asm.Label) { - return nil, nil -} + loc, err := mapper.OffsetLocation(id.Offset, id.End()) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil + } + } + } -// The assembler allows center dot (· U+00B7) and -// division slash (∕ U+2215) to work as identifier characters. -func isIdentRune(r rune) bool { - return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '·' || r == '∕' + return nil, nil } // TODO(rfindley): avoid the duplicate column mapping here, by associating a diff --git a/gopls/internal/golang/assembly.go b/gopls/internal/golang/assembly.go index 9e673dd9719..12244a58c59 100644 --- a/gopls/internal/golang/assembly.go +++ b/gopls/internal/golang/assembly.go @@ -10,6 +10,9 @@ package golang // - ./codeaction.go - computes the symbol and offers the CodeAction command. // - ../server/command.go - handles the command by opening a web page. // - ../server/server.go - handles the HTTP request and calls this function. +// +// For language-server behavior in Go assembly language files, +// see [golang.org/x/tools/gopls/internal/goasm]. import ( "bytes" diff --git a/gopls/internal/test/marker/testdata/definition/asm.txt b/gopls/internal/test/marker/testdata/definition/asm.txt index f0187d7e24a..250f237d299 100644 --- a/gopls/internal/test/marker/testdata/definition/asm.txt +++ b/gopls/internal/test/marker/testdata/definition/asm.txt @@ -26,6 +26,9 @@ var _ = ff // pacify unusedfunc analyzer TEXT ·ff(SB), $16 //@ loc(ffasm, "ff"), def("ff", ffgo) CALL example·com∕b·B //@ def("com", bB) JMP ·ff //@ def("ff", ffgo) + JMP label //@ def("label", label) +label: //@ loc(label,"label") + RET -- b/b.go -- package b diff --git a/gopls/internal/util/asm/parse.go b/gopls/internal/util/asm/parse.go new file mode 100644 index 00000000000..11c59a7cc3d --- /dev/null +++ b/gopls/internal/util/asm/parse.go @@ -0,0 +1,245 @@ +// 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 asm provides a simple parser for Go assembly files. +package asm + +import ( + "bufio" + "bytes" + "fmt" + "strings" + "unicode" +) + +// Kind describes the nature of an identifier in an assembly file. +type Kind uint8 + +const ( + Invalid Kind = iota // reserved zero value; not used by Ident + Ref // arbitrary reference to symbol or control label + Text // definition of TEXT (function) symbol + Global // definition of GLOBL (var) symbol + Data // initialization of GLOBL (var) symbol; effectively a reference + Label // definition of control label +) + +func (k Kind) String() string { + if int(k) < len(kindString) { + return kindString[k] + } + return fmt.Sprintf("Kind(%d)", k) +} + +var kindString = [...]string{ + Invalid: "invalid", + Ref: "ref", + Text: "text", + Global: "global", + Data: "data", + Label: "label", +} + +// A file represents a parsed file of Go assembly language. +type File struct { + Idents []Ident + + // TODO(adonovan): use token.File? This may be important in a + // future in which analyzers can report diagnostics in .s files. +} + +// Ident represents an identifier in an assembly file. +type Ident struct { + Name string // symbol name (after correcting [·∕]); Name[0]='.' => current package + Offset int // zero-based byte offset + Kind Kind +} + +// End returns the identifier's end offset. +func (id Ident) End() int { return id.Offset + len(id.Name) } + +// Parse extracts identifiers from Go assembly files. +// Since it is a best-effort parser, it never returns an error. +func Parse(content []byte) *File { + var idents []Ident + offset := 0 // byte offset of start of current line + + // TODO(adonovan) use a proper tokenizer that respects + // comments, string literals, line continuations, etc. + scan := bufio.NewScanner(bytes.NewReader(content)) + for ; scan.Scan(); offset += len(scan.Bytes()) + len("\n") { + line := scan.Text() + + // Strip comments. + if idx := strings.Index(line, "//"); idx >= 0 { + line = line[:idx] + } + + // Skip blank lines. + if strings.TrimSpace(line) == "" { + continue + } + + // Check for label definitions (ending with colon). + if colon := strings.IndexByte(line, ':'); colon > 0 { + label := strings.TrimSpace(line[:colon]) + if isIdent(label) { + idents = append(idents, Ident{ + Name: label, + Offset: offset + strings.Index(line, label), + Kind: Label, + }) + continue + } + } + + // Split line into words. + words := strings.Fields(line) + if len(words) == 0 { + continue + } + + // A line of the form + // TEXT ·sym(SB),NOSPLIT,$12 + // declares a text symbol "·sym". + if len(words) > 1 { + kind := Invalid + switch words[0] { + case "TEXT": + kind = Text + case "GLOBL": + kind = Global + case "DATA": + kind = Data + } + if kind != Invalid { + sym := words[1] + sym = cutBefore(sym, ",") // strip ",NOSPLIT,$12" etc + sym = cutBefore(sym, "(") // "sym(SB)" -> "sym" + sym = cutBefore(sym, "<") // "sym" -> "sym" + sym = strings.TrimSpace(sym) + if isIdent(sym) { + // (The Index call assumes sym is not itself "TEXT" etc.) + idents = append(idents, Ident{ + Name: cleanup(sym), + Kind: kind, + Offset: offset + strings.Index(line, sym), + }) + } + continue + } + } + + // Find references in the rest of the line. + pos := 0 + for _, word := range words { + // Find actual position of word within line. + tokenPos := strings.Index(line[pos:], word) + if tokenPos < 0 { + panic(line) + } + tokenPos += pos + pos = tokenPos + len(word) + + // Reject probable instruction mnemonics (e.g. MOV). + if len(word) >= 2 && word[0] != '·' && + !strings.ContainsFunc(word, unicode.IsLower) { + continue + } + + if word[0] == '$' { + word = word[1:] + tokenPos++ + + // Reject probable immediate values (e.g. "$123"). + if !strings.ContainsFunc(word, isNonDigit) { + continue + } + } + + // Reject probably registers (e.g. "PC"). + if len(word) <= 3 && !strings.ContainsFunc(word, unicode.IsLower) { + continue + } + + // Probable identifier reference. + // + // TODO(adonovan): handle FP symbols correctly; + // sym+8(FP) is essentially a comment about + // stack slot 8, not a reference to a symbol + // with a declaration somewhere; so they form + // an equivalence class without a canonical + // declaration. + // + // TODO(adonovan): handle pseudoregisters and field + // references such as: + // MOVD $runtime·g0(SB), g // pseudoreg + // MOVD R0, g_stackguard0(g) // field ref + + sym := cutBefore(word, "(") // "·sym(SB)" => "sym" + sym = cutBefore(sym, "+") // "sym+8(FP)" => "sym" + sym = cutBefore(sym, "<") // "sym" =>> "sym" + if isIdent(sym) { + idents = append(idents, Ident{ + Name: cleanup(sym), + Kind: Ref, + Offset: offset + tokenPos, + }) + } + } + } + + _ = scan.Err() // ignore scan errors + + return &File{Idents: idents} +} + +// isIdent reports whether s is a valid Go assembly identifier. +func isIdent(s string) bool { + for i, r := range s { + if !isIdentRune(r, i) { + return false + } + } + return len(s) > 0 +} + +// cutBefore returns the portion of s before the first occurrence of sep, if any. +func cutBefore(s, sep string) string { + if before, _, ok := strings.Cut(s, sep); ok { + return before + } + return s +} + +// cleanup converts a symbol name from assembler syntax to linker syntax. +func cleanup(sym string) string { + return repl.Replace(sym) +} + +var repl = strings.NewReplacer( + "·", ".", // (U+00B7 MIDDLE DOT) + "∕", "/", // (U+2215 DIVISION SLASH) +) + +func isNonDigit(r rune) bool { return !unicode.IsDigit(r) } + +// -- plundered from GOROOT/src/cmd/asm/internal/asm/parse.go -- + +// We want center dot (·) and division slash (∕) to work as identifier characters. +func isIdentRune(ch rune, i int) bool { + if unicode.IsLetter(ch) { + return true + } + switch ch { + case '_': // Underscore; traditional. + return true + case '\u00B7': // Represents the period in runtime.exit. U+00B7 '·' middle dot + return true + case '\u2215': // Represents the slash in runtime/debug.setGCPercent. U+2215 '∕' division slash + return true + } + // Digits are OK only after the first character. + return i > 0 && unicode.IsDigit(ch) +} diff --git a/gopls/internal/util/asm/parse_test.go b/gopls/internal/util/asm/parse_test.go new file mode 100644 index 00000000000..67a1286d28b --- /dev/null +++ b/gopls/internal/util/asm/parse_test.go @@ -0,0 +1,67 @@ +// 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 asm_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/asm" +) + +// TestIdents checks that (likely) identifiers are extracted in the expected places. +func TestIdents(t *testing.T) { + src := []byte(` +// This is a nonsense file containing a variety of syntax. + +#include "foo.h" +#ifdef MACRO +DATA hello<>+0x00(SB)/64, $"Hello" +GLOBL hello<(SB), RODATA, $64 +#endif + +TEXT mypkg·f(SB),NOSPLIT,$0 + MOVD R1, 16(RSP) // another comment + MOVD $otherpkg·data(SB), R2 + JMP label +label: + BL ·g(SB) + +TEXT ·g(SB),NOSPLIT,$0 + MOVD $runtime·g0(SB), g + MOVD R0, g_stackguard0(g) + MOVD R0, (g_stack+stack_lo)(g) +`[1:]) + const filename = "asm.s" + m := protocol.NewMapper(protocol.URIFromPath(filename), src) + file := asm.Parse(src) + + want := ` +asm.s:5:6-11: data "hello" +asm.s:6:7-12: global "hello" +asm.s:9:6-13: text "mypkg.f" +asm.s:11:8-21: ref "otherpkg.data" +asm.s:12:6-11: ref "label" +asm.s:13:1-6: label "label" +asm.s:14:5-7: ref ".g" +asm.s:16:6-8: text ".g" +asm.s:17:8-18: ref "runtime.g0" +asm.s:17:25-26: ref "g" +asm.s:18:11-24: ref "g_stackguard0" +`[1:] + var buf bytes.Buffer + for _, id := range file.Idents { + line, col := m.OffsetLineCol8(id.Offset) + _, endCol := m.OffsetLineCol8(id.Offset + len(id.Name)) + fmt.Fprintf(&buf, "%s:%d:%d-%d:\t%s %q\n", filename, line, col, endCol, id.Kind, id.Name) + } + got := buf.String() + if got != want { + t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, cmp.Diff(want, got)) + } +} From 8d38122b0b1a9991f490aa06b7bfca7b4140bdad Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 3 Mar 2025 22:13:31 +0000 Subject: [PATCH 93/99] gopls/internal/cache: reproduce and fix crash on if cond overflow Through reverse engineering, I was able to reproduce the overflow of golang/go#72026, and verify the fix of CL 653596. Along the way, I incidentally reproduced golang/go#66766, which I think we can safely ignore now that we understand it. Updates golang/go#72026 Fixes golang/go#66766 Change-Id: I2131d771c13688c1ad47f6bc6285e524fb4c04a1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/654336 Reviewed-by: Alan Donovan Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/check.go | 15 +++++++-------- gopls/internal/cache/parsego/parse.go | 1 + .../integration/completion/fixedbugs_test.go | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index aa1537c8705..27d5cfa240b 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -2005,15 +2005,14 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs *typeCheckInputs, errs [ posn := safetoken.StartPosition(e.Fset, start) if !posn.IsValid() { // All valid positions produced by the type checker should described by - // its fileset. + // its fileset, yet since type checker errors are associated with + // positions in the AST, and AST nodes can overflow the file + // (golang/go#48300), we can't rely on this. // - // Note: in golang/go#64488, we observed an error that was positioned - // over fixed syntax, which overflowed its file. So it's definitely - // possible that we get here (it's hard to reason about fixing up the - // AST). Nevertheless, it's a bug. - if pkg.hasFixedFiles() { - bug.Reportf("internal error: type checker error %q outside its Fset (fixed files)", e) - } else { + // We should fix the parser, but in the meantime type errors are not + // significant if there are parse errors, so we can safely ignore this + // case. + if len(pkg.parseErrors) == 0 { bug.Reportf("internal error: type checker error %q outside its Fset", e) } continue diff --git a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go index fd598e235d1..4b37816caff 100644 --- a/gopls/internal/cache/parsego/parse.go +++ b/gopls/internal/cache/parsego/parse.go @@ -532,6 +532,7 @@ func fixInitStmt(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) if err != nil { return false } + assert(end <= len(src), "offset overflow") // golang/go#72026 stmtBytes := src[start:end] stmt, err := parseStmt(tok, bad.Pos(), stmtBytes) if err != nil { diff --git a/gopls/internal/test/integration/completion/fixedbugs_test.go b/gopls/internal/test/integration/completion/fixedbugs_test.go index faa5324e138..ccec432904e 100644 --- a/gopls/internal/test/integration/completion/fixedbugs_test.go +++ b/gopls/internal/test/integration/completion/fixedbugs_test.go @@ -38,3 +38,20 @@ package } }) } + +func TestFixInitStatementCrash_Issue72026(t *testing.T) { + // This test checks that we don't crash when the if condition overflows the + // file (as is possible with a malformed struct type). + + const files = ` +-- go.mod -- +module example.com + +go 1.18 +` + + Run(t, files, func(t *testing.T, env *Env) { + env.CreateBuffer("p.go", "package p\nfunc _() {\n\tfor i := struct") + env.AfterChange() + }) +} From 07219402b2fc707689574d91ee3cfd2c9a544a87 Mon Sep 17 00:00:00 2001 From: xieyuschen Date: Tue, 4 Mar 2025 19:05:13 +0800 Subject: [PATCH 94/99] gopls/internal/analysis/modernize: strings.Fields -> FieldsSeq This CL enhances the existing modernizer to support calls to strings.Fields and bytes.Fields, that offers a fix to instead use go1.24's FieldsSeq, which avoids allocating an array. Fixes golang/go#72033 Change-Id: I2059f66f38a639c5a264b650137ced7b4f84550e Reviewed-on: https://go-review.googlesource.com/c/tools/+/654535 Auto-Submit: Alan Donovan Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Junyang Shao --- gopls/doc/analyzers.md | 2 +- gopls/internal/analysis/modernize/doc.go | 2 +- .../internal/analysis/modernize/modernize.go | 2 +- .../analysis/modernize/modernize_test.go | 1 + .../modernize/{splitseq.go => stringsseq.go} | 31 +++++++++----- .../testdata/src/fieldsseq/fieldsseq.go | 42 +++++++++++++++++++ .../src/fieldsseq/fieldsseq.go.golden | 42 +++++++++++++++++++ .../testdata/src/fieldsseq/fieldsseq_go123.go | 1 + gopls/internal/doc/api.json | 4 +- 9 files changed, 111 insertions(+), 16 deletions(-) rename gopls/internal/analysis/modernize/{splitseq.go => stringsseq.go} (77%) create mode 100644 gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go create mode 100644 gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go.golden create mode 100644 gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq_go123.go diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index dde95591718..aa95e024089 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -498,7 +498,7 @@ existing code by using more modern features of Go, such as: - replacing a 3-clause for i := 0; i < n; i++ {} loop by for i := range n {}, added in go1.22; - replacing Split in "for range strings.Split(...)" by go1.24's - more efficient SplitSeq; + more efficient SplitSeq, or Fields with FieldSeq; To apply all modernization fixes en masse, you can use the following command: diff --git a/gopls/internal/analysis/modernize/doc.go b/gopls/internal/analysis/modernize/doc.go index 3759fdb10c5..b12abab7063 100644 --- a/gopls/internal/analysis/modernize/doc.go +++ b/gopls/internal/analysis/modernize/doc.go @@ -31,7 +31,7 @@ // - replacing a 3-clause for i := 0; i < n; i++ {} loop by // for i := range n {}, added in go1.22; // - replacing Split in "for range strings.Split(...)" by go1.24's -// more efficient SplitSeq; +// more efficient SplitSeq, or Fields with FieldSeq; // // To apply all modernization fixes en masse, you can use the // following command: diff --git a/gopls/internal/analysis/modernize/modernize.go b/gopls/internal/analysis/modernize/modernize.go index 354836d6b40..96e8b325df4 100644 --- a/gopls/internal/analysis/modernize/modernize.go +++ b/gopls/internal/analysis/modernize/modernize.go @@ -72,7 +72,7 @@ func run(pass *analysis.Pass) (any, error) { rangeint(pass) slicescontains(pass) slicesdelete(pass) - splitseq(pass) + stringsseq(pass) sortslice(pass) testingContext(pass) diff --git a/gopls/internal/analysis/modernize/modernize_test.go b/gopls/internal/analysis/modernize/modernize_test.go index 6662914b28d..7bdc8014389 100644 --- a/gopls/internal/analysis/modernize/modernize_test.go +++ b/gopls/internal/analysis/modernize/modernize_test.go @@ -24,6 +24,7 @@ func Test(t *testing.T) { "slicescontains", "slicesdelete", "splitseq", + "fieldsseq", "sortslice", "testingcontext", ) diff --git a/gopls/internal/analysis/modernize/splitseq.go b/gopls/internal/analysis/modernize/stringsseq.go similarity index 77% rename from gopls/internal/analysis/modernize/splitseq.go rename to gopls/internal/analysis/modernize/stringsseq.go index 1f3da859e9b..ca9d918912e 100644 --- a/gopls/internal/analysis/modernize/splitseq.go +++ b/gopls/internal/analysis/modernize/stringsseq.go @@ -5,6 +5,7 @@ package modernize import ( + "fmt" "go/ast" "go/token" "go/types" @@ -17,8 +18,9 @@ import ( "golang.org/x/tools/internal/astutil/edge" ) -// splitseq offers a fix to replace a call to strings.Split with -// SplitSeq when it is the operand of a range loop, either directly: +// stringsseq offers a fix to replace a call to strings.Split with +// SplitSeq or strings.Fields with FieldsSeq +// when it is the operand of a range loop, either directly: // // for _, line := range strings.Split() {...} // @@ -29,7 +31,8 @@ import ( // // Variants: // - bytes.SplitSeq -func splitseq(pass *analysis.Pass) { +// - bytes.FieldsSeq +func stringsseq(pass *analysis.Pass) { if !analysisinternal.Imports(pass.Pkg, "strings") && !analysisinternal.Imports(pass.Pkg, "bytes") { return @@ -88,21 +91,27 @@ func splitseq(pass *analysis.Pass) { }) } - if sel, ok := call.Fun.(*ast.SelectorExpr); ok && - (analysisinternal.IsFunctionNamed(typeutil.Callee(info, call), "strings", "Split") || - analysisinternal.IsFunctionNamed(typeutil.Callee(info, call), "bytes", "Split")) { + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + + obj := typeutil.Callee(info, call) + if analysisinternal.IsFunctionNamed(obj, "strings", "Split", "Fields") || + analysisinternal.IsFunctionNamed(obj, "bytes", "Split", "Fields") { + oldFnName := obj.Name() + seqFnName := fmt.Sprintf("%sSeq", oldFnName) pass.Report(analysis.Diagnostic{ Pos: sel.Pos(), End: sel.End(), - Category: "splitseq", - Message: "Ranging over SplitSeq is more efficient", + Category: "stringsseq", + Message: fmt.Sprintf("Ranging over %s is more efficient", seqFnName), SuggestedFixes: []analysis.SuggestedFix{{ - Message: "Replace Split with SplitSeq", + Message: fmt.Sprintf("Replace %s with %s", oldFnName, seqFnName), TextEdits: append(edits, analysis.TextEdit{ - // Split -> SplitSeq Pos: sel.Sel.Pos(), End: sel.Sel.End(), - NewText: []byte("SplitSeq")}), + NewText: []byte(seqFnName)}), }}, }) } diff --git a/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go new file mode 100644 index 00000000000..b86df1a8a94 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go @@ -0,0 +1,42 @@ +//go:build go1.24 + +package fieldsseq + +import ( + "bytes" + "strings" +) + +func _() { + for _, line := range strings.Fields("") { // want "Ranging over FieldsSeq is more efficient" + println(line) + } + for i, line := range strings.Fields("") { // nope: uses index var + println(i, line) + } + for i, _ := range strings.Fields("") { // nope: uses index var + println(i) + } + for i := range strings.Fields("") { // nope: uses index var + println(i) + } + for _ = range strings.Fields("") { // want "Ranging over FieldsSeq is more efficient" + } + for range strings.Fields("") { // want "Ranging over FieldsSeq is more efficient" + } + for range bytes.Fields(nil) { // want "Ranging over FieldsSeq is more efficient" + } + { + lines := strings.Fields("") // want "Ranging over FieldsSeq is more efficient" + for _, line := range lines { + println(line) + } + } + { + lines := strings.Fields("") // nope: lines is used not just by range + for _, line := range lines { + println(line) + } + println(lines) + } +} diff --git a/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go.golden b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go.golden new file mode 100644 index 00000000000..9fa1bfd1b62 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq.go.golden @@ -0,0 +1,42 @@ +//go:build go1.24 + +package fieldsseq + +import ( + "bytes" + "strings" +) + +func _() { + for line := range strings.FieldsSeq("") { // want "Ranging over FieldsSeq is more efficient" + println(line) + } + for i, line := range strings.Fields( "") { // nope: uses index var + println(i, line) + } + for i, _ := range strings.Fields( "") { // nope: uses index var + println(i) + } + for i := range strings.Fields( "") { // nope: uses index var + println(i) + } + for range strings.FieldsSeq("") { // want "Ranging over FieldsSeq is more efficient" + } + for range strings.FieldsSeq("") { // want "Ranging over FieldsSeq is more efficient" + } + for range bytes.FieldsSeq(nil) { // want "Ranging over FieldsSeq is more efficient" + } + { + lines := strings.FieldsSeq("") // want "Ranging over FieldsSeq is more efficient" + for line := range lines { + println(line) + } + } + { + lines := strings.Fields( "") // nope: lines is used not just by range + for _, line := range lines { + println(line) + } + println(lines) + } +} diff --git a/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq_go123.go b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq_go123.go new file mode 100644 index 00000000000..c2bd314db75 --- /dev/null +++ b/gopls/internal/analysis/modernize/testdata/src/fieldsseq/fieldsseq_go123.go @@ -0,0 +1 @@ +package fieldsseq diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 5775d0d4361..4001e3605bb 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -514,7 +514,7 @@ }, { "Name": "\"modernize\"", - "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq;\n\nTo apply all modernization fixes en masse, you can use the\nfollowing command:\n\n\t$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./...\n\nIf the tool warns of conflicting fixes, you may need to run it more\nthan once until it has applied all fixes cleanly. This command is\nnot an officially supported interface and may change in the future.", + "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq, or Fields with FieldSeq;\n\nTo apply all modernization fixes en masse, you can use the\nfollowing command:\n\n\t$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./...\n\nIf the tool warns of conflicting fixes, you may need to run it more\nthan once until it has applied all fixes cleanly. This command is\nnot an officially supported interface and may change in the future.", "Default": "true" }, { @@ -1228,7 +1228,7 @@ }, { "Name": "modernize", - "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq;\n\nTo apply all modernization fixes en masse, you can use the\nfollowing command:\n\n\t$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./...\n\nIf the tool warns of conflicting fixes, you may need to run it more\nthan once until it has applied all fixes cleanly. This command is\nnot an officially supported interface and may change in the future.", + "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq, or Fields with FieldSeq;\n\nTo apply all modernization fixes en masse, you can use the\nfollowing command:\n\n\t$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./...\n\nIf the tool warns of conflicting fixes, you may need to run it more\nthan once until it has applied all fixes cleanly. This command is\nnot an officially supported interface and may change in the future.", "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize", "Default": true }, From 340f21a49b9cad20d07a2b58e483a991084dc481 Mon Sep 17 00:00:00 2001 From: xieyuschen Date: Wed, 5 Mar 2025 12:14:02 +0800 Subject: [PATCH 95/99] gopls: move gopls/doc/generate package This CL tracks adonovan's TODO by moving generate package from gopls/doc/generate to gopls/internal/doc/generate. Change-Id: I08fc90859cc6afe10ab5ac658a7b8a514d36cc32 Reviewed-on: https://go-review.googlesource.com/c/tools/+/654536 Reviewed-by: Alan Donovan Reviewed-by: Junyang Shao Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/doc/api.go | 2 +- gopls/{ => internal}/doc/generate/generate.go | 4 +--- gopls/{ => internal}/doc/generate/generate_test.go | 0 3 files changed, 2 insertions(+), 4 deletions(-) rename gopls/{ => internal}/doc/generate/generate.go (99%) rename gopls/{ => internal}/doc/generate/generate_test.go (100%) diff --git a/gopls/internal/doc/api.go b/gopls/internal/doc/api.go index 258f90d49ae..5011d2172ed 100644 --- a/gopls/internal/doc/api.go +++ b/gopls/internal/doc/api.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:generate go run ../../doc/generate +//go:generate go run ./generate // The doc package provides JSON metadata that documents gopls' public // interfaces. diff --git a/gopls/doc/generate/generate.go b/gopls/internal/doc/generate/generate.go similarity index 99% rename from gopls/doc/generate/generate.go rename to gopls/internal/doc/generate/generate.go index b0d3e8c49f6..51c8b89e39b 100644 --- a/gopls/doc/generate/generate.go +++ b/gopls/internal/doc/generate/generate.go @@ -11,9 +11,7 @@ // // Run it with this command: // -// $ cd gopls/internal/doc && go generate -// -// TODO(adonovan): move this package to gopls/internal/doc/generate. +// $ cd gopls/internal/doc/generate && go generate package main import ( diff --git a/gopls/doc/generate/generate_test.go b/gopls/internal/doc/generate/generate_test.go similarity index 100% rename from gopls/doc/generate/generate_test.go rename to gopls/internal/doc/generate/generate_test.go From ece9e9ba0760eb361376c8a890b24e89db031d9e Mon Sep 17 00:00:00 2001 From: Hongxiang Jiang Date: Tue, 25 Feb 2025 13:48:15 -0500 Subject: [PATCH 96/99] gopls/doc/generate: add status in codelenses and inlayhints Features configurable through map[K]V can not be marked as experimental. To comply with deprecation guideline, this CL introduces a per key and per value status where gopls can mark a specific key or a specific value as experimental. The status can be indicated by the comment directives as part of the doc comment. The status can be delcared following pattern "//gopls:status X" very similar to struct tag. This clarifies the question: if "codelenses" is a released feature, are all enum keys configurable in "codelenses" are also released feature? VSCode-Go CL 652357 Change-Id: I4ddc5155751452d5f7b92bbb3610aa61680a29a4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/652356 Auto-Submit: Hongxiang Jiang Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/doc/codelenses.md | 4 + gopls/internal/analysis/gofix/directive.go | 95 ------ gopls/internal/analysis/gofix/gofix.go | 3 +- gopls/internal/doc/api.go | 8 +- gopls/internal/doc/api.json | 351 ++++++++++++++------- gopls/internal/doc/generate/generate.go | 55 +++- gopls/internal/settings/settings.go | 4 + internal/astutil/comment.go | 85 +++++ 8 files changed, 375 insertions(+), 230 deletions(-) delete mode 100644 gopls/internal/analysis/gofix/directive.go diff --git a/gopls/doc/codelenses.md b/gopls/doc/codelenses.md index d8aa8e1f479..fa7c6c68859 100644 --- a/gopls/doc/codelenses.md +++ b/gopls/doc/codelenses.md @@ -75,6 +75,8 @@ File type: Go ## `run_govulncheck`: Run govulncheck (legacy) +**This setting is experimental and may be deleted.** + This codelens source annotates the `module` directive in a go.mod file with a command to run Govulncheck asynchronously. @@ -134,6 +136,8 @@ File type: go.mod ## `vulncheck`: Run govulncheck +**This setting is experimental and may be deleted.** + This codelens source annotates the `module` directive in a go.mod file with a command to run govulncheck synchronously. diff --git a/gopls/internal/analysis/gofix/directive.go b/gopls/internal/analysis/gofix/directive.go deleted file mode 100644 index 20c45313cfb..00000000000 --- a/gopls/internal/analysis/gofix/directive.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gofix - -import ( - "go/ast" - "go/token" - "strings" -) - -// -- plundered from the future (CL 605517, issue #68021) -- - -// TODO(adonovan): replace with ast.Directive after go1.25 (#68021). -// Beware of our local mods to handle analysistest -// "want" comments on the same line. - -// A directive is a comment line with special meaning to the Go -// toolchain or another tool. It has the form: -// -// //tool:name args -// -// The "tool:" portion is missing for the three directives named -// line, extern, and export. -// -// See https://go.dev/doc/comment#Syntax for details of Go comment -// syntax and https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives -// for details of directives used by the Go compiler. -type directive struct { - Pos token.Pos // of preceding "//" - Tool string - Name string - Args string // may contain internal spaces -} - -// directives returns the directives within the comment. -func directives(g *ast.CommentGroup) (res []*directive) { - if g != nil { - // Avoid (*ast.CommentGroup).Text() as it swallows directives. - for _, c := range g.List { - if len(c.Text) > 2 && - c.Text[1] == '/' && - c.Text[2] != ' ' && - isDirective(c.Text[2:]) { - - tool, nameargs, ok := strings.Cut(c.Text[2:], ":") - if !ok { - // Must be one of {line,extern,export}. - tool, nameargs = "", tool - } - name, args, _ := strings.Cut(nameargs, " ") // tab?? - // Permit an additional line comment after the args, chiefly to support - // [golang.org/x/tools/go/analysis/analysistest]. - args, _, _ = strings.Cut(args, "//") - res = append(res, &directive{ - Pos: c.Slash, - Tool: tool, - Name: name, - Args: strings.TrimSpace(args), - }) - } - } - } - return -} - -// isDirective reports whether c is a comment directive. -// This code is also in go/printer. -func isDirective(c string) bool { - // "//line " is a line directive. - // "//extern " is for gccgo. - // "//export " is for cgo. - // (The // has been removed.) - if strings.HasPrefix(c, "line ") || strings.HasPrefix(c, "extern ") || strings.HasPrefix(c, "export ") { - return true - } - - // "//[a-z0-9]+:[a-z0-9]" - // (The // has been removed.) - colon := strings.Index(c, ":") - if colon <= 0 || colon+1 >= len(c) { - return false - } - for i := 0; i <= colon+1; i++ { - if i == colon { - continue - } - b := c[i] - if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') { - return false - } - } - return true -} diff --git a/gopls/internal/analysis/gofix/gofix.go b/gopls/internal/analysis/gofix/gofix.go index 41cebcb63b9..a2380f1d644 100644 --- a/gopls/internal/analysis/gofix/gofix.go +++ b/gopls/internal/analysis/gofix/gofix.go @@ -20,6 +20,7 @@ import ( "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/util/moreiters" "golang.org/x/tools/internal/analysisinternal" + internalastutil "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/astutil/edge" "golang.org/x/tools/internal/diff" @@ -598,7 +599,7 @@ func currentFile(c cursor.Cursor) *ast.File { // hasFixInline reports the presence of a "//go:fix inline" directive // in the comments. func hasFixInline(cg *ast.CommentGroup) bool { - for _, d := range directives(cg) { + for _, d := range internalastutil.Directives(cg) { if d.Tool == "go" && d.Name == "fix" && d.Args == "inline" { return true } diff --git a/gopls/internal/doc/api.go b/gopls/internal/doc/api.go index 5011d2172ed..52101dda8c9 100644 --- a/gopls/internal/doc/api.go +++ b/gopls/internal/doc/api.go @@ -47,11 +47,13 @@ type EnumKey struct { Name string // in JSON syntax (quoted) Doc string Default string + Status string // = "" | "advanced" | "experimental" | "deprecated" } type EnumValue struct { - Value string // in JSON syntax (quoted) - Doc string // doc comment; always starts with `Value` + Value string // in JSON syntax (quoted) + Doc string // doc comment; always starts with `Value` + Status string // = "" | "advanced" | "experimental" | "deprecated" } type Lens struct { @@ -60,6 +62,7 @@ type Lens struct { Title string Doc string Default bool + Status string // = "" | "advanced" | "experimental" | "deprecated" } type Analyzer struct { @@ -73,4 +76,5 @@ type Hint struct { Name string Doc string Default bool + Status string // = "" | "advanced" | "experimental" | "deprecated" } diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 4001e3605bb..b9e0e78e950 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -124,23 +124,28 @@ "EnumValues": [ { "Value": "\"FullDocumentation\"", - "Doc": "" + "Doc": "", + "Status": "" }, { "Value": "\"NoDocumentation\"", - "Doc": "" + "Doc": "", + "Status": "" }, { "Value": "\"SingleLine\"", - "Doc": "" + "Doc": "", + "Status": "" }, { "Value": "\"Structured\"", - "Doc": "`\"Structured\"` is a misguided experimental setting that returns a JSON\nhover format. This setting should not be used, as it will be removed in a\nfuture release of gopls.\n" + "Doc": "`\"Structured\"` is a misguided experimental setting that returns a JSON\nhover format. This setting should not be used, as it will be removed in a\nfuture release of gopls.\n", + "Status": "" }, { "Value": "\"SynopsisDocumentation\"", - "Doc": "" + "Doc": "", + "Status": "" } ], "Default": "\"FullDocumentation\"", @@ -173,15 +178,18 @@ "EnumValues": [ { "Value": "false", - "Doc": "false: do not show links" + "Doc": "false: do not show links", + "Status": "" }, { "Value": "true", - "Doc": "true: show links to the `linkTarget` domain" + "Doc": "true: show links to the `linkTarget` domain", + "Status": "" }, { "Value": "\"gopls\"", - "Doc": "`\"gopls\"`: show links to gopls' internal documentation viewer" + "Doc": "`\"gopls\"`: show links to gopls' internal documentation viewer", + "Status": "" } ], "Default": "true", @@ -228,15 +236,18 @@ "EnumValues": [ { "Value": "\"CaseInsensitive\"", - "Doc": "" + "Doc": "", + "Status": "" }, { "Value": "\"CaseSensitive\"", - "Doc": "" + "Doc": "", + "Status": "" }, { "Value": "\"Fuzzy\"", - "Doc": "" + "Doc": "", + "Status": "" } ], "Default": "\"Fuzzy\"", @@ -283,15 +294,18 @@ "EnumValues": [ { "Value": "\"Both\"", - "Doc": "" + "Doc": "", + "Status": "" }, { "Value": "\"Definition\"", - "Doc": "" + "Doc": "", + "Status": "" }, { "Value": "\"Link\"", - "Doc": "" + "Doc": "", + "Status": "" } ], "Default": "\"Both\"", @@ -310,19 +324,23 @@ "EnumValues": [ { "Value": "\"CaseInsensitive\"", - "Doc": "" + "Doc": "", + "Status": "" }, { "Value": "\"CaseSensitive\"", - "Doc": "" + "Doc": "", + "Status": "" }, { "Value": "\"FastFuzzy\"", - "Doc": "" + "Doc": "", + "Status": "" }, { "Value": "\"Fuzzy\"", - "Doc": "" + "Doc": "", + "Status": "" } ], "Default": "\"FastFuzzy\"", @@ -341,15 +359,18 @@ "EnumValues": [ { "Value": "\"Dynamic\"", - "Doc": "`\"Dynamic\"` uses whichever qualifier results in the highest scoring\nmatch for the given symbol query. Here a \"qualifier\" is any \"/\" or \".\"\ndelimited suffix of the fully qualified symbol. i.e. \"to/pkg.Foo.Field\" or\njust \"Foo.Field\".\n" + "Doc": "`\"Dynamic\"` uses whichever qualifier results in the highest scoring\nmatch for the given symbol query. Here a \"qualifier\" is any \"/\" or \".\"\ndelimited suffix of the fully qualified symbol. i.e. \"to/pkg.Foo.Field\" or\njust \"Foo.Field\".\n", + "Status": "" }, { "Value": "\"Full\"", - "Doc": "`\"Full\"` is fully qualified symbols, i.e.\n\"path/to/pkg.Foo.Field\".\n" + "Doc": "`\"Full\"` is fully qualified symbols, i.e.\n\"path/to/pkg.Foo.Field\".\n", + "Status": "" }, { "Value": "\"Package\"", - "Doc": "`\"Package\"` is package qualified symbols i.e.\n\"pkg.Foo.Field\".\n" + "Doc": "`\"Package\"` is package qualified symbols i.e.\n\"pkg.Foo.Field\".\n", + "Status": "" } ], "Default": "\"Dynamic\"", @@ -368,11 +389,13 @@ "EnumValues": [ { "Value": "\"all\"", - "Doc": "`\"all\"` matches symbols in any loaded package, including\ndependencies.\n" + "Doc": "`\"all\"` matches symbols in any loaded package, including\ndependencies.\n", + "Status": "" }, { "Value": "\"workspace\"", - "Doc": "`\"workspace\"` matches symbols in workspace packages only.\n" + "Doc": "`\"workspace\"` matches symbols in workspace packages only.\n", + "Status": "" } ], "Default": "\"all\"", @@ -390,282 +413,338 @@ { "Name": "\"appends\"", "Doc": "check for missing values after append\n\nThis checker reports calls to append that pass\nno values to be appended to the slice.\n\n\ts := []string{\"a\", \"b\", \"c\"}\n\t_ = append(s)\n\nSuch calls are always no-ops and often indicate an\nunderlying mistake.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"asmdecl\"", "Doc": "report mismatches between assembly files and Go declarations", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"assign\"", "Doc": "check for useless assignments\n\nThis checker reports assignments of the form x = x or a[i] = a[i].\nThese are almost always useless, and even when they aren't they are\nusually a mistake.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"atomic\"", "Doc": "check for common mistakes using the sync/atomic package\n\nThe atomic checker looks for assignment statements of the form:\n\n\tx = atomic.AddUint64(\u0026x, 1)\n\nwhich are not atomic.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"atomicalign\"", "Doc": "check for non-64-bits-aligned arguments to sync/atomic functions", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"bools\"", "Doc": "check for common mistakes involving boolean operators", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"buildtag\"", "Doc": "check //go:build and // +build directives", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"cgocall\"", "Doc": "detect some violations of the cgo pointer passing rules\n\nCheck for invalid cgo pointer passing.\nThis looks for code that uses cgo to call C code passing values\nwhose types are almost always invalid according to the cgo pointer\nsharing rules.\nSpecifically, it warns about attempts to pass a Go chan, map, func,\nor slice to C, either directly, or via a pointer, array, or struct.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"composites\"", "Doc": "check for unkeyed composite literals\n\nThis analyzer reports a diagnostic for composite literals of struct\ntypes imported from another package that do not use the field-keyed\nsyntax. Such literals are fragile because the addition of a new field\n(even if unexported) to the struct will cause compilation to fail.\n\nAs an example,\n\n\terr = \u0026net.DNSConfigError{err}\n\nshould be replaced by:\n\n\terr = \u0026net.DNSConfigError{Err: err}\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"copylocks\"", "Doc": "check for locks erroneously passed by value\n\nInadvertently copying a value containing a lock, such as sync.Mutex or\nsync.WaitGroup, may cause both copies to malfunction. Generally such\nvalues should be referred to through a pointer.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"deepequalerrors\"", "Doc": "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"defers\"", "Doc": "report common mistakes in defer statements\n\nThe defers analyzer reports a diagnostic when a defer statement would\nresult in a non-deferred call to time.Since, as experience has shown\nthat this is nearly always a mistake.\n\nFor example:\n\n\tstart := time.Now()\n\t...\n\tdefer recordLatency(time.Since(start)) // error: call to time.Since is not deferred\n\nThe correct code is:\n\n\tdefer func() { recordLatency(time.Since(start)) }()", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"deprecated\"", "Doc": "check for use of deprecated identifiers\n\nThe deprecated analyzer looks for deprecated symbols and package\nimports.\n\nSee https://go.dev/wiki/Deprecated to learn about Go's convention\nfor documenting and signaling deprecated identifiers.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"directive\"", "Doc": "check Go toolchain directives such as //go:debug\n\nThis analyzer checks for problems with known Go toolchain directives\nin all Go source files in a package directory, even those excluded by\n//go:build constraints, and all non-Go source files too.\n\nFor //go:debug (see https://go.dev/doc/godebug), the analyzer checks\nthat the directives are placed only in Go source files, only above the\npackage comment, and only in package main or *_test.go files.\n\nSupport for other known directives may be added in the future.\n\nThis analyzer does not check //go:build, which is handled by the\nbuildtag analyzer.\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"embed\"", "Doc": "check //go:embed directive usage\n\nThis analyzer checks that the embed package is imported if //go:embed\ndirectives are present, providing a suggested fix to add the import if\nit is missing.\n\nThis analyzer also checks that //go:embed directives precede the\ndeclaration of a single variable.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"errorsas\"", "Doc": "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analysis reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"fillreturns\"", "Doc": "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\n\nwill turn into\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"framepointer\"", "Doc": "report assembly that clobbers the frame pointer before saving it", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"gofix\"", "Doc": "apply fixes based on go:fix comment directives\n\nThe gofix analyzer inlines functions and constants that are marked for inlining.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"hostport\"", "Doc": "check format of addresses passed to net.Dial\n\nThis analyzer flags code that produce network address strings using\nfmt.Sprintf, as in this example:\n\n addr := fmt.Sprintf(\"%s:%d\", host, 12345) // \"will not work with IPv6\"\n ...\n conn, err := net.Dial(\"tcp\", addr) // \"when passed to dial here\"\n\nThe analyzer suggests a fix to use the correct approach, a call to\nnet.JoinHostPort:\n\n addr := net.JoinHostPort(host, \"12345\")\n ...\n conn, err := net.Dial(\"tcp\", addr)\n\nA similar diagnostic and fix are produced for a format string of \"%s:%s\".\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"httpresponse\"", "Doc": "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"ifaceassert\"", "Doc": "detect impossible interface-to-interface type assertions\n\nThis checker flags type assertions v.(T) and corresponding type-switch cases\nin which the static type V of v is an interface that cannot possibly implement\nthe target interface T. This occurs when V and T contain methods with the same\nname but different signatures. Example:\n\n\tvar v interface {\n\t\tRead()\n\t}\n\t_ = v.(io.Reader)\n\nThe Read method in v has a different signature than the Read method in\nio.Reader, so this assertion cannot succeed.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"infertypeargs\"", "Doc": "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"loopclosure\"", "Doc": "check references to loop variables from within nested functions\n\nThis analyzer reports places where a function literal references the\niteration variable of an enclosing loop, and the loop calls the function\nin such a way (e.g. with go or defer) that it may outlive the loop\niteration and possibly observe the wrong value of the variable.\n\nNote: An iteration variable can only outlive a loop iteration in Go versions \u003c=1.21.\nIn Go 1.22 and later, the loop variable lifetimes changed to create a new\niteration variable per loop iteration. (See go.dev/issue/60078.)\n\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v [\u003cgo1.22].\n\n\tfor _, v := range list {\n\t defer func() {\n\t use(v) // incorrect\n\t }()\n\t}\n\nOne fix is to create a new variable for each iteration of the loop:\n\n\tfor _, v := range list {\n\t v := v // new var per iteration\n\t defer func() {\n\t use(v) // ok\n\t }()\n\t}\n\nAfter Go version 1.22, the previous two for loops are equivalent\nand both are correct.\n\nThe next example uses a go statement and has a similar problem [\u003cgo1.22].\nIn addition, it has a data race because the loop updates v\nconcurrent with the goroutines accessing it.\n\n\tfor _, v := range elem {\n\t go func() {\n\t use(v) // incorrect, and a data race\n\t }()\n\t}\n\nA fix is the same as before. The checker also reports problems\nin goroutines started by golang.org/x/sync/errgroup.Group.\nA hard-to-spot variant of this form is common in parallel tests:\n\n\tfunc Test(t *testing.T) {\n\t for _, test := range tests {\n\t t.Run(test.name, func(t *testing.T) {\n\t t.Parallel()\n\t use(test) // incorrect, and a data race\n\t })\n\t }\n\t}\n\nThe t.Parallel() call causes the rest of the function to execute\nconcurrent with the loop [\u003cgo1.22].\n\nThe analyzer reports references only in the last statement,\nas it is not deep enough to understand the effects of subsequent\nstatements that might render the reference benign.\n(\"Last statement\" is defined recursively in compound\nstatements such as if, switch, and select.)\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"lostcancel\"", "Doc": "check cancel func returned by context.WithCancel is called\n\nThe cancellation function returned by context.WithCancel, WithTimeout,\nWithDeadline and variants such as WithCancelCause must be called,\nor the new context will remain live until its parent context is cancelled.\n(The background context is never cancelled.)", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"modernize\"", "Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq, or Fields with FieldSeq;\n\nTo apply all modernization fixes en masse, you can use the\nfollowing command:\n\n\t$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./...\n\nIf the tool warns of conflicting fixes, you may need to run it more\nthan once until it has applied all fixes cleanly. This command is\nnot an officially supported interface and may change in the future.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"nilfunc\"", "Doc": "check for useless comparisons between functions and nil\n\nA useless comparison is one like f == nil as opposed to f() == nil.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"nilness\"", "Doc": "check for redundant or impossible nil comparisons\n\nThe nilness checker inspects the control-flow graph of each function in\na package and reports nil pointer dereferences, degenerate nil\npointers, and panics with nil values. A degenerate comparison is of the form\nx==nil or x!=nil where x is statically known to be nil or non-nil. These are\noften a mistake, especially in control flow related to errors. Panics with nil\nvalues are checked because they are not detectable by\n\n\tif r := recover(); r != nil {\n\nThis check reports conditions such as:\n\n\tif f == nil { // impossible condition (f is a function)\n\t}\n\nand:\n\n\tp := \u0026v\n\t...\n\tif p != nil { // tautological condition\n\t}\n\nand:\n\n\tif p == nil {\n\t\tprint(*p) // nil dereference\n\t}\n\nand:\n\n\tif p == nil {\n\t\tpanic(p)\n\t}\n\nSometimes the control flow may be quite complex, making bugs hard\nto spot. In the example below, the err.Error expression is\nguaranteed to panic because, after the first return, err must be\nnil. The intervening loop is just a distraction.\n\n\t...\n\terr := g.Wait()\n\tif err != nil {\n\t\treturn err\n\t}\n\tpartialSuccess := false\n\tfor _, err := range errs {\n\t\tif err == nil {\n\t\t\tpartialSuccess = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif partialSuccess {\n\t\treportStatus(StatusMessage{\n\t\t\tCode: code.ERROR,\n\t\t\tDetail: err.Error(), // \"nil dereference in dynamic method call\"\n\t\t})\n\t\treturn nil\n\t}\n\n...", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"nonewvars\"", "Doc": "suggested fixes for \"no new vars on left side of :=\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no new vars on left side of :=\". For example:\n\n\tz := 1\n\tz := 2\n\nwill turn into\n\n\tz := 1\n\tz = 2", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"noresultvalues\"", "Doc": "suggested fixes for unexpected return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"no result values expected\" or \"too many return values\".\nFor example:\n\n\tfunc z() { return nil }\n\nwill turn into\n\n\tfunc z() { return }", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"printf\"", "Doc": "check consistency of Printf format strings and arguments\n\nThe check applies to calls of the formatting functions such as\n[fmt.Printf] and [fmt.Sprintf], as well as any detected wrappers of\nthose functions such as [log.Printf]. It reports a variety of\nmistakes such as syntax errors in the format string and mismatches\n(of number and type) between the verbs and their arguments.\n\nSee the documentation of the fmt package for the complete set of\nformat operators and their operand types.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"shadow\"", "Doc": "check for possible unintended shadowing of variables\n\nThis analyzer check for shadowed variables.\nA shadowed variable is a variable declared in an inner scope\nwith the same name and type as a variable in an outer scope,\nand where the outer variable is mentioned after the inner one\nis declared.\n\n(This definition can be refined; the module generates too many\nfalse positives and is not yet enabled by default.)\n\nFor example:\n\n\tfunc BadRead(f *os.File, buf []byte) error {\n\t\tvar err error\n\t\tfor {\n\t\t\tn, err := f.Read(buf) // shadows the function variable 'err'\n\t\t\tif err != nil {\n\t\t\t\tbreak // causes return of wrong value\n\t\t\t}\n\t\t\tfoo(buf)\n\t\t}\n\t\treturn err\n\t}", - "Default": "false" + "Default": "false", + "Status": "" }, { "Name": "\"shift\"", "Doc": "check for shifts that equal or exceed the width of the integer", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"sigchanyzer\"", "Doc": "check for unbuffered channel of os.Signal\n\nThis checker reports call expression of the form\n\n\tsignal.Notify(c \u003c-chan os.Signal, sig ...os.Signal),\n\nwhere c is an unbuffered channel, which can be at risk of missing the signal.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"simplifycompositelit\"", "Doc": "check for composite literal simplifications\n\nAn array, slice, or map composite literal of the form:\n\n\t[]T{T{}, T{}}\n\nwill be simplified to:\n\n\t[]T{{}, {}}\n\nThis is one of the simplifications that \"gofmt -s\" applies.\n\nThis analyzer ignores generated code.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"simplifyrange\"", "Doc": "check for range statement simplifications\n\nA range of the form:\n\n\tfor x, _ = range v {...}\n\nwill be simplified to:\n\n\tfor x = range v {...}\n\nA range of the form:\n\n\tfor _ = range v {...}\n\nwill be simplified to:\n\n\tfor range v {...}\n\nThis is one of the simplifications that \"gofmt -s\" applies.\n\nThis analyzer ignores generated code.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"simplifyslice\"", "Doc": "check for slice simplifications\n\nA slice expression of the form:\n\n\ts[a:len(s)]\n\nwill be simplified to:\n\n\ts[a:]\n\nThis is one of the simplifications that \"gofmt -s\" applies.\n\nThis analyzer ignores generated code.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"slog\"", "Doc": "check for invalid structured logging calls\n\nThe slog checker looks for calls to functions from the log/slog\npackage that take alternating key-value pairs. It reports calls\nwhere an argument in a key position is neither a string nor a\nslog.Attr, and where a final key is missing its value.\nFor example,it would report\n\n\tslog.Warn(\"message\", 11, \"k\") // slog.Warn arg \"11\" should be a string or a slog.Attr\n\nand\n\n\tslog.Info(\"message\", \"k1\", v1, \"k2\") // call to slog.Info missing a final value", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"sortslice\"", "Doc": "check the argument type of sort.Slice\n\nsort.Slice requires an argument of a slice type. Check that\nthe interface{} value passed to sort.Slice is actually a slice.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"stdmethods\"", "Doc": "check signature of methods of well-known interfaces\n\nSometimes a type may be intended to satisfy an interface but may fail to\ndo so because of a mistake in its method signature.\nFor example, the result of this WriteTo method should be (int64, error),\nnot error, to satisfy io.WriterTo:\n\n\ttype myWriterTo struct{...}\n\tfunc (myWriterTo) WriteTo(w io.Writer) error { ... }\n\nThis check ensures that each method whose name matches one of several\nwell-known interface methods from the standard library has the correct\nsignature for that interface.\n\nChecked method names include:\n\n\tFormat GobEncode GobDecode MarshalJSON MarshalXML\n\tPeek ReadByte ReadFrom ReadRune Scan Seek\n\tUnmarshalJSON UnreadByte UnreadRune WriteByte\n\tWriteTo", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"stdversion\"", "Doc": "report uses of too-new standard library symbols\n\nThe stdversion analyzer reports references to symbols in the standard\nlibrary that were introduced by a Go release higher than the one in\nforce in the referring file. (Recall that the file's Go version is\ndefined by the 'go' directive its module's go.mod file, or by a\n\"//go:build go1.X\" build tag at the top of the file.)\n\nThe analyzer does not report a diagnostic for a reference to a \"too\nnew\" field or method of a type that is itself \"too new\", as this may\nhave false positives, for example if fields or methods are accessed\nthrough a type alias that is guarded by a Go version constraint.\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"stringintconv\"", "Doc": "check for string(int) conversions\n\nThis checker flags conversions of the form string(x) where x is an integer\n(but not byte or rune) type. Such conversions are discouraged because they\nreturn the UTF-8 representation of the Unicode code point x, and not a decimal\nstring representation of x as one might expect. Furthermore, if x denotes an\ninvalid code point, the conversion cannot be statically rejected.\n\nFor conversions that intend on using the code point, consider replacing them\nwith string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the\nstring representation of the value in the desired base.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"structtag\"", "Doc": "check that struct field tags conform to reflect.StructTag.Get\n\nAlso report certain struct tags (json, xml) used with unexported fields.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"testinggoroutine\"", "Doc": "report calls to (*testing.T).Fatal from goroutines started by a test\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\n\tfunc TestFoo(t *testing.T) {\n\t go func() {\n\t t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n\t }()\n\t}", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"tests\"", "Doc": "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark, Fuzzing and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"timeformat\"", "Doc": "check for calls of (time.Time).Format or time.Parse with 2006-02-01\n\nThe timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)\nformat. Internationally, \"yyyy-dd-mm\" does not occur in common calendar date\nstandards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"unmarshal\"", "Doc": "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"unreachable\"", "Doc": "check for unreachable code\n\nThe unreachable analyzer finds statements that execution can never reach\nbecause they are preceded by a return statement, a call to panic, an\ninfinite loop, or similar constructs.", - "Default": "true" + "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.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"unusedfunc\"", "Doc": "check for unused functions and methods\n\nThe unusedfunc analyzer reports functions and methods that are\nnever referenced outside of their own declaration.\n\nA function is considered unused if it is unexported and not\nreferenced (except within its own declaration).\n\nA method is considered unused if it is unexported, not referenced\n(except within its own declaration), and its name does not match\nthat of any method of an interface type declared within the same\npackage.\n\nThe tool may report false positives in some situations, for\nexample:\n\n - For a declaration of an unexported function that is referenced\n from another package using the go:linkname mechanism, if the\n declaration's doc comment does not also have a go:linkname\n comment.\n\n (Such code is in any case strongly discouraged: linkname\n annotations, if they must be used at all, should be used on both\n the declaration and the alias.)\n\n - For compiler intrinsics in the \"runtime\" package that, though\n never referenced, are known to the compiler and are called\n indirectly by compiled object code.\n\n - For functions called only from assembly.\n\n - For functions called only from files whose build tags are not\n selected in the current build configuration.\n\nSee https://github.com/golang/go/issues/71686 for discussion of\nthese limitations.\n\nThe unusedfunc algorithm is not as precise as the\ngolang.org/x/tools/cmd/deadcode tool, but it has the advantage that\nit runs within the modular analysis framework, enabling near\nreal-time feedback within gopls.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"unusedparams\"", "Doc": "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo ensure soundness, it ignores:\n - \"address-taken\" functions, that is, functions that are used as\n a value rather than being called directly; their signatures may\n be required to conform to a func type.\n - exported functions or methods, since they may be address-taken\n in another package.\n - unexported methods whose name matches an interface method\n declared in the same package, since the method's signature\n may be required to conform to the interface type.\n - functions with empty bodies, or containing just a call to panic.\n - parameters that are unnamed, or named \"_\", the blank identifier.\n\nThe analyzer suggests a fix of replacing the parameter name by \"_\",\nbut in such cases a deeper fix can be obtained by invoking the\n\"Refactor: remove unused parameter\" code action, which will\neliminate the parameter entirely, along with all corresponding\narguments at call sites, while taking care to preserve any side\neffects in the argument expressions; see\nhttps://github.com/golang/tools/releases/tag/gopls%2Fv0.14.\n\nThis analyzer ignores generated code.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"unusedresult\"", "Doc": "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side\neffects, so it is always a mistake to discard the result. Other\nfunctions may return an error that must not be ignored, or a cleanup\noperation that must be called. This analyzer reports calls to\nfunctions like these when the result of the call is ignored.\n\nThe set of functions may be controlled using flags.", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"unusedvariable\"", "Doc": "check for unused variables and suggest fixes", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"unusedwrite\"", "Doc": "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\n\tfunc f(input []T) {\n\t\tfor i, v := range input { // v is a copy\n\t\t\tv.x = i // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\n\tfunc (t T) f() { // t is a copy\n\t\tt.x = i // unused write to field x\n\t}", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"waitgroup\"", "Doc": "check for misuses of sync.WaitGroup\n\nThis analyzer detects mistaken calls to the (*sync.WaitGroup).Add\nmethod from inside a new goroutine, causing Add to race with Wait:\n\n\t// WRONG\n\tvar wg sync.WaitGroup\n\tgo func() {\n\t wg.Add(1) // \"WaitGroup.Add called from inside new goroutine\"\n\t defer wg.Done()\n\t ...\n\t}()\n\twg.Wait() // (may return prematurely before new goroutine starts)\n\nThe correct code calls Add before starting the goroutine:\n\n\t// RIGHT\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t...\n\t}()\n\twg.Wait()", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"yield\"", "Doc": "report calls to yield where the result is ignored\n\nAfter a yield function returns false, the caller should not call\nthe yield function again; generally the iterator should return\npromptly.\n\nThis example fails to check the result of the call to yield,\ncausing this analyzer to report a diagnostic:\n\n\tyield(1) // yield may be called again (on L2) after returning false\n\tyield(2)\n\nThe corrected code is either this:\n\n\tif yield(1) { yield(2) }\n\nor simply:\n\n\t_ = yield(1) \u0026\u0026 yield(2)\n\nIt is not always a mistake to ignore the result of yield.\nFor example, this is a valid single-element iterator:\n\n\tyield(1) // ok to ignore result\n\treturn\n\nIt is only a mistake when the yield call that returned false may be\nfollowed by another call.", - "Default": "true" + "Default": "true", + "Status": "" } ] }, @@ -699,22 +778,26 @@ { "Name": "\"bounds\"", "Doc": "`\"bounds\"` controls bounds checking diagnostics.\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"escape\"", "Doc": "`\"escape\"` controls diagnostics about escape choices.\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"inline\"", "Doc": "`\"inline\"` controls diagnostics about inlining choices.\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"nil\"", "Doc": "`\"nil\"` controls nil checks.\n", - "Default": "true" + "Default": "true", + "Status": "" } ] }, @@ -735,11 +818,13 @@ "EnumValues": [ { "Value": "\"Imports\"", - "Doc": "`\"Imports\"`: In Imports mode, `gopls` will report vulnerabilities that affect packages\ndirectly and indirectly used by the analyzed main module.\n" + "Doc": "`\"Imports\"`: In Imports mode, `gopls` will report vulnerabilities that affect packages\ndirectly and indirectly used by the analyzed main module.\n", + "Status": "" }, { "Value": "\"Off\"", - "Doc": "`\"Off\"`: Disable vulnerability analysis.\n" + "Doc": "`\"Off\"`: Disable vulnerability analysis.\n", + "Status": "" } ], "Default": "\"Off\"", @@ -772,11 +857,13 @@ "EnumValues": [ { "Value": "\"Edit\"", - "Doc": "`\"Edit\"`: Trigger diagnostics on file edit and save. (default)\n" + "Doc": "`\"Edit\"`: Trigger diagnostics on file edit and save. (default)\n", + "Status": "" }, { "Value": "\"Save\"", - "Doc": "`\"Save\"`: Trigger diagnostics only on file save. Events like initial workspace load\nor configuration change will still trigger diagnostics.\n" + "Doc": "`\"Save\"`: Trigger diagnostics only on file save. Events like initial workspace load\nor configuration change will still trigger diagnostics.\n", + "Status": "" } ], "Default": "\"Edit\"", @@ -808,37 +895,44 @@ { "Name": "\"assignVariableTypes\"", "Doc": "`\"assignVariableTypes\"` controls inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```\n", - "Default": "false" + "Default": "false", + "Status": "" }, { "Name": "\"compositeLiteralFields\"", "Doc": "`\"compositeLiteralFields\"` inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```\n", - "Default": "false" + "Default": "false", + "Status": "" }, { "Name": "\"compositeLiteralTypes\"", "Doc": "`\"compositeLiteralTypes\"` controls inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```\n", - "Default": "false" + "Default": "false", + "Status": "" }, { "Name": "\"constantValues\"", "Doc": "`\"constantValues\"` controls inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```\n", - "Default": "false" + "Default": "false", + "Status": "" }, { "Name": "\"functionTypeParameters\"", "Doc": "`\"functionTypeParameters\"` inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```\n", - "Default": "false" + "Default": "false", + "Status": "" }, { "Name": "\"parameterNames\"", "Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```\n", - "Default": "false" + "Default": "false", + "Status": "" }, { "Name": "\"rangeVariableTypes\"", "Doc": "`\"rangeVariableTypes\"` controls inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```\n", - "Default": "false" + "Default": "false", + "Status": "" } ] }, @@ -858,42 +952,50 @@ { "Name": "\"generate\"", "Doc": "`\"generate\"`: Run `go generate`\n\nThis codelens source annotates any `//go:generate` comments\nwith commands to run `go generate` in this directory, on\nall directories recursively beneath this one.\n\nSee [Generating code](https://go.dev/blog/generate) for\nmore details.\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"regenerate_cgo\"", "Doc": "`\"regenerate_cgo\"`: Re-generate cgo declarations\n\nThis codelens source annotates an `import \"C\"` declaration\nwith a command to re-run the [cgo\ncommand](https://pkg.go.dev/cmd/cgo) to regenerate the\ncorresponding Go declarations.\n\nUse this after editing the C code in comments attached to\nthe import, or in C header files included by it.\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"run_govulncheck\"", "Doc": "`\"run_govulncheck\"`: Run govulncheck (legacy)\n\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run Govulncheck asynchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n", - "Default": "false" + "Default": "false", + "Status": "experimental" }, { "Name": "\"test\"", "Doc": "`\"test\"`: Run tests and benchmarks\n\nThis codelens source annotates each `Test` and `Benchmark`\nfunction in a `*_test.go` file with a command to run it.\n\nThis source is off by default because VS Code has\na client-side custom UI for testing, and because progress\nnotifications are not a great UX for streamed test output.\nSee:\n- golang/go#67400 for a discussion of this feature.\n- https://github.com/joaotavora/eglot/discussions/1402\n for an alternative approach.\n", - "Default": "false" + "Default": "false", + "Status": "" }, { "Name": "\"tidy\"", "Doc": "`\"tidy\"`: Tidy go.mod file\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\ntidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures\nthat the go.mod file matches the source code in the module.\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"upgrade_dependency\"", "Doc": "`\"upgrade_dependency\"`: Update dependencies\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with commands to:\n\n- check for available upgrades,\n- upgrade direct dependencies, and\n- upgrade all dependencies transitively.\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"vendor\"", "Doc": "`\"vendor\"`: Update vendor directory\n\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\nvendor`](https://go.dev/ref/mod#go-mod-vendor), which\ncreates or updates the directory named `vendor` in the\nmodule root so that it contains an up-to-date copy of all\nnecessary package dependencies.\n", - "Default": "true" + "Default": "true", + "Status": "" }, { "Name": "\"vulncheck\"", "Doc": "`\"vulncheck\"`: Run govulncheck\n\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run govulncheck synchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n", - "Default": "false" + "Default": "false", + "Status": "experimental" } ] }, @@ -1023,56 +1125,64 @@ "Lens": "generate", "Title": "Run `go generate`", "Doc": "\nThis codelens source annotates any `//go:generate` comments\nwith commands to run `go generate` in this directory, on\nall directories recursively beneath this one.\n\nSee [Generating code](https://go.dev/blog/generate) for\nmore details.\n", - "Default": true + "Default": true, + "Status": "" }, { "FileType": "Go", "Lens": "regenerate_cgo", "Title": "Re-generate cgo declarations", "Doc": "\nThis codelens source annotates an `import \"C\"` declaration\nwith a command to re-run the [cgo\ncommand](https://pkg.go.dev/cmd/cgo) to regenerate the\ncorresponding Go declarations.\n\nUse this after editing the C code in comments attached to\nthe import, or in C header files included by it.\n", - "Default": true + "Default": true, + "Status": "" }, { "FileType": "Go", "Lens": "test", "Title": "Run tests and benchmarks", "Doc": "\nThis codelens source annotates each `Test` and `Benchmark`\nfunction in a `*_test.go` file with a command to run it.\n\nThis source is off by default because VS Code has\na client-side custom UI for testing, and because progress\nnotifications are not a great UX for streamed test output.\nSee:\n- golang/go#67400 for a discussion of this feature.\n- https://github.com/joaotavora/eglot/discussions/1402\n for an alternative approach.\n", - "Default": false + "Default": false, + "Status": "" }, { "FileType": "go.mod", "Lens": "run_govulncheck", "Title": "Run govulncheck (legacy)", "Doc": "\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run Govulncheck asynchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n", - "Default": false + "Default": false, + "Status": "experimental" }, { "FileType": "go.mod", "Lens": "tidy", "Title": "Tidy go.mod file", "Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\ntidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures\nthat the go.mod file matches the source code in the module.\n", - "Default": true + "Default": true, + "Status": "" }, { "FileType": "go.mod", "Lens": "upgrade_dependency", "Title": "Update dependencies", "Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with commands to:\n\n- check for available upgrades,\n- upgrade direct dependencies, and\n- upgrade all dependencies transitively.\n", - "Default": true + "Default": true, + "Status": "" }, { "FileType": "go.mod", "Lens": "vendor", "Title": "Update vendor directory", "Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\nvendor`](https://go.dev/ref/mod#go-mod-vendor), which\ncreates or updates the directory named `vendor` in the\nmodule root so that it contains an up-to-date copy of all\nnecessary package dependencies.\n", - "Default": true + "Default": true, + "Status": "" }, { "FileType": "go.mod", "Lens": "vulncheck", "Title": "Run govulncheck", "Doc": "\nThis codelens source annotates the `module` directive in a go.mod file\nwith a command to run govulncheck synchronously.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static analysis tool that\ncomputes the set of functions reachable within your application, including\ndependencies; queries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n", - "Default": false + "Default": false, + "Status": "experimental" } ], "Analyzers": [ @@ -1417,37 +1527,44 @@ { "Name": "assignVariableTypes", "Doc": "`\"assignVariableTypes\"` controls inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```\n", - "Default": false + "Default": false, + "Status": "" }, { "Name": "compositeLiteralFields", "Doc": "`\"compositeLiteralFields\"` inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```\n", - "Default": false + "Default": false, + "Status": "" }, { "Name": "compositeLiteralTypes", "Doc": "`\"compositeLiteralTypes\"` controls inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```\n", - "Default": false + "Default": false, + "Status": "" }, { "Name": "constantValues", "Doc": "`\"constantValues\"` controls inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```\n", - "Default": false + "Default": false, + "Status": "" }, { "Name": "functionTypeParameters", "Doc": "`\"functionTypeParameters\"` inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```\n", - "Default": false + "Default": false, + "Status": "" }, { "Name": "parameterNames", "Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```\n", - "Default": false + "Default": false, + "Status": "" }, { "Name": "rangeVariableTypes", "Doc": "`\"rangeVariableTypes\"` controls inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```\n", - "Default": false + "Default": false, + "Status": "" } ] } \ No newline at end of file diff --git a/gopls/internal/doc/generate/generate.go b/gopls/internal/doc/generate/generate.go index 51c8b89e39b..762fceeb4b9 100644 --- a/gopls/internal/doc/generate/generate.go +++ b/gopls/internal/doc/generate/generate.go @@ -317,9 +317,17 @@ func loadEnums(pkg *packages.Package) (map[types.Type][]doc.EnumValue, error) { spec := path[1].(*ast.ValueSpec) value := cnst.Val().ExactString() docstring := valueDoc(cnst.Name(), value, spec.Doc.Text()) + var status string + for _, d := range internalastutil.Directives(spec.Doc) { + if d.Tool == "gopls" && d.Name == "status" { + status = d.Args + break + } + } v := doc.EnumValue{ - Value: value, - Doc: docstring, + Value: value, + Doc: docstring, + Status: status, } enums[obj.Type()] = append(enums[obj.Type()], v) } @@ -354,6 +362,7 @@ func collectEnumKeys(m *types.Map, reflectField reflect.Value, enumValues []doc. keys = append(keys, doc.EnumKey{ Name: v.Value, Doc: v.Doc, + Status: v.Status, Default: def, }) } @@ -436,6 +445,7 @@ func loadLenses(settingsPkg *packages.Package, defaults map[settings.CodeLensSou // Find the CodeLensSource enums among the files of the protocol package. // Map each enum value to its doc comment. enumDoc := make(map[string]string) + enumStatus := make(map[string]string) for _, f := range settingsPkg.Syntax { for _, decl := range f.Decls { if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.CONST { @@ -455,6 +465,12 @@ func loadLenses(settingsPkg *packages.Package, defaults map[settings.CodeLensSou return nil, fmt.Errorf("%s: %s lacks doc comment", posn, spec.Names[0].Name) } enumDoc[value] = spec.Doc.Text() + for _, d := range internalastutil.Directives(spec.Doc) { + if d.Tool == "gopls" && d.Name == "status" { + enumStatus[value] = d.Args + break + } + } } } } @@ -479,6 +495,7 @@ func loadLenses(settingsPkg *packages.Package, defaults map[settings.CodeLensSou Title: title, Doc: docText, Default: defaults[source], + Status: enumStatus[string(source)], }) } return nil @@ -518,8 +535,9 @@ func loadHints(settingsPkg *packages.Package) ([]*doc.Hint, error) { for _, enumVal := range enums[inlayHint] { name, _ := strconv.Unquote(enumVal.Value) hints = append(hints, &doc.Hint{ - Name: name, - Doc: enumVal.Doc, + Name: name, + Doc: enumVal.Doc, + Status: enumVal.Status, }) } return hints, nil @@ -600,17 +618,7 @@ func rewriteSettings(prevContent []byte, api *doc.API) ([]byte, error) { fmt.Fprintf(&buf, "### `%s %s`\n\n", opt.Name, opt.Type) // status - switch opt.Status { - case "": - case "advanced": - fmt.Fprint(&buf, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n") - case "debug": - fmt.Fprint(&buf, "**This setting is for debugging purposes only.**\n\n") - case "experimental": - fmt.Fprint(&buf, "**This setting is experimental and may be deleted.**\n\n") - default: - fmt.Fprintf(&buf, "**Status: %s.**\n\n", opt.Status) - } + writeStatus(&buf, opt.Status) // doc comment buf.WriteString(opt.Doc) @@ -651,6 +659,22 @@ func rewriteSettings(prevContent []byte, api *doc.API) ([]byte, error) { return content, nil } +// writeStatus emits a Markdown paragraph to buf about the status of a feature, +// if nonempty. +func writeStatus(buf *bytes.Buffer, status string) { + switch status { + case "": + case "advanced": + fmt.Fprint(buf, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n") + case "debug": + fmt.Fprint(buf, "**This setting is for debugging purposes only.**\n\n") + case "experimental": + fmt.Fprint(buf, "**This setting is experimental and may be deleted.**\n\n") + default: + fmt.Fprintf(buf, "**Status: %s.**\n\n", status) + } +} + var parBreakRE = regexp.MustCompile("\n{2,}") func shouldShowEnumKeysInSettings(name string) bool { @@ -722,6 +746,7 @@ func rewriteCodeLenses(prevContent []byte, api *doc.API) ([]byte, error) { var buf bytes.Buffer for _, lens := range api.Lenses { fmt.Fprintf(&buf, "## `%s`: %s\n\n", lens.Lens, lens.Title) + writeStatus(&buf, lens.Status) fmt.Fprintf(&buf, "%s\n\n", lens.Doc) fmt.Fprintf(&buf, "Default: %v\n\n", onOff(lens.Default)) fmt.Fprintf(&buf, "File type: %s\n\n", lens.FileType) diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index e98bc365935..59b2aa1b87f 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -269,6 +269,8 @@ const ( // computes the set of functions reachable within your application, including // dependencies; queries a database of known security vulnerabilities; and // reports any potential problems it finds. + // + //gopls:status experimental CodeLensVulncheck CodeLensSource = "vulncheck" // Run govulncheck (legacy) @@ -280,6 +282,8 @@ const ( // computes the set of functions reachable within your application, including // dependencies; queries a database of known security vulnerabilities; and // reports any potential problems it finds. + // + //gopls:status experimental CodeLensRunGovulncheck CodeLensSource = "run_govulncheck" // Run tests and benchmarks diff --git a/internal/astutil/comment.go b/internal/astutil/comment.go index 192d6430de0..ee4be23f226 100644 --- a/internal/astutil/comment.go +++ b/internal/astutil/comment.go @@ -6,6 +6,7 @@ package astutil import ( "go/ast" + "go/token" "strings" ) @@ -26,3 +27,87 @@ func Deprecation(doc *ast.CommentGroup) string { } return "" } + +// -- plundered from the future (CL 605517, issue #68021) -- + +// TODO(adonovan): replace with ast.Directive after go1.25 (#68021). +// Beware of our local mods to handle analysistest +// "want" comments on the same line. + +// A directive is a comment line with special meaning to the Go +// toolchain or another tool. It has the form: +// +// //tool:name args +// +// The "tool:" portion is missing for the three directives named +// line, extern, and export. +// +// See https://go.dev/doc/comment#Syntax for details of Go comment +// syntax and https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives +// for details of directives used by the Go compiler. +type Directive struct { + Pos token.Pos // of preceding "//" + Tool string + Name string + Args string // may contain internal spaces +} + +// isDirective reports whether c is a comment directive. +// This code is also in go/printer. +func isDirective(c string) bool { + // "//line " is a line directive. + // "//extern " is for gccgo. + // "//export " is for cgo. + // (The // has been removed.) + if strings.HasPrefix(c, "line ") || strings.HasPrefix(c, "extern ") || strings.HasPrefix(c, "export ") { + return true + } + + // "//[a-z0-9]+:[a-z0-9]" + // (The // has been removed.) + colon := strings.Index(c, ":") + if colon <= 0 || colon+1 >= len(c) { + return false + } + for i := 0; i <= colon+1; i++ { + if i == colon { + continue + } + b := c[i] + if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') { + return false + } + } + return true +} + +// Directives returns the directives within the comment. +func Directives(g *ast.CommentGroup) (res []*Directive) { + if g != nil { + // Avoid (*ast.CommentGroup).Text() as it swallows directives. + for _, c := range g.List { + if len(c.Text) > 2 && + c.Text[1] == '/' && + c.Text[2] != ' ' && + isDirective(c.Text[2:]) { + + tool, nameargs, ok := strings.Cut(c.Text[2:], ":") + if !ok { + // Must be one of {line,extern,export}. + tool, nameargs = "", tool + } + name, args, _ := strings.Cut(nameargs, " ") // tab?? + // Permit an additional line comment after the args, chiefly to support + // [golang.org/x/tools/go/analysis/analysistest]. + args, _, _ = strings.Cut(args, "//") + res = append(res, &Directive{ + Pos: c.Slash, + Tool: tool, + Name: name, + Args: strings.TrimSpace(args), + }) + } + } + } + return +} From db6008cb90f09485deb11255e5dd6da114b4ecef Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 5 Mar 2025 13:18:07 -0500 Subject: [PATCH 97/99] go/types/internal/play: show Cursor.Stack of selected node Change-Id: Iaf6a6369e05ded0b10b85f468d8fbf91269373e4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/655135 Reviewed-by: Jonathan Amsterdam Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- go/types/internal/play/play.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go index f1318ac247a..4212a6b82cf 100644 --- a/go/types/internal/play/play.go +++ b/go/types/internal/play/play.go @@ -30,8 +30,10 @@ import ( "strings" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/astutil/cursor" "golang.org/x/tools/internal/typeparams" ) @@ -161,6 +163,15 @@ func handleSelectJSON(w http.ResponseWriter, req *http.Request) { innermostExpr = e } } + // Show the cursor stack too. + // It's usually the same, but may differ in edge + // cases (e.g. around FuncType.Func). + inspect := inspector.New([]*ast.File{file}) + if cur, ok := cursor.Root(inspect).FindPos(startPos, endPos); ok { + fmt.Fprintf(out, "Cursor.FindPos().Stack() = %v\n", cur.Stack(nil)) + } else { + fmt.Fprintf(out, "Cursor.FindPos() failed\n") + } fmt.Fprintf(out, "\n") // Expression type information From 25a90befcdf96d15f13dd947b7395c8531dc67de Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 3 Mar 2025 23:06:21 -0500 Subject: [PATCH 98/99] gopls/internal/golang: Implementations for func types This CL adds support to the Implementations query for function types. The query relates two sets of locations: 1. the "func" token of each function declaration (FuncDecl or FuncLit). These are analogous to declarations of concrete methods. 2. uses of abstract functions: (a) the "func" token of each FuncType that is not part of Func{Decl,Lit}. These are analogous to interface{...} types. (b) the "(" paren of each dynamic call on a value of an abstract function type. These are analogous to references to interface method names, but no names are involved, which has historically made them hard to search for. An Implementations query on a location in set 1 returns set 2, and vice versa. Only the local algorithm is implemented for now; the global one (using an index analogous to methodsets) will follow. This CL supersedes CL 448035 and CL 619515, both of which attempt to unify the treatment of functions and interfaces in the methodsets algorithm and in the index; but the two problems are not precisely analogous, and I think we'll end up with more but simpler code if we implement themn separately. + tests, docs, relnotes Updates golang/go#56572 Change-Id: I18e1a7cc2f6c320112b9f3589323d04f9a52ef3c Reviewed-on: https://go-review.googlesource.com/c/tools/+/654556 Commit-Queue: Alan Donovan Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- gopls/doc/features/navigation.md | 22 +- gopls/doc/release/v0.19.0.md | 27 ++ gopls/internal/golang/implementation.go | 291 ++++++++++++++++-- gopls/internal/test/marker/doc.go | 7 +- gopls/internal/test/marker/marker_test.go | 10 +- .../testdata/implementation/signature.txt | 79 +++++ 6 files changed, 403 insertions(+), 33 deletions(-) create mode 100644 gopls/internal/test/marker/testdata/implementation/signature.txt diff --git a/gopls/doc/features/navigation.md b/gopls/doc/features/navigation.md index f46f2935683..f3454f7188c 100644 --- a/gopls/doc/features/navigation.md +++ b/gopls/doc/features/navigation.md @@ -85,7 +85,10 @@ Client support: The LSP [`textDocument/implementation`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_implementation) -request queries the "implements" relation between interfaces and concrete types: +request queries the relation between abstract and concrete types and +their methods. + +Interfaces and concrete types are matched using method sets: - When invoked on a reference to an **interface type**, it returns the location of the declaration of each type that implements @@ -111,6 +114,17 @@ types with methods due to embedding) may be missing from the results. but that is not consistent with the "scalable" gopls design. --> +Functions, `func` types, and dynamic function calls are matched using signatures: + +- When invoked on the `func` token of a **function definition**, + it returns the locations of the matching signature types + and dynamic call expressions. +- When invoked on the `func` token of a **signature type**, + it returns the locations of the matching concrete function definitions. +- When invoked on the `(` token of a **dynamic function call**, + it returns the locations of the matching concrete function + definitions. + If either the target type or the candidate type are generic, the results will include the candidate type if there is any instantiation of the two types that would allow one to implement the other. @@ -120,6 +134,12 @@ types, without regard to consistency of substitutions across the method set or even within a single method. This may lead to occasional spurious matches.) +Since a type may be both a function type and a named type with methods +(for example, `http.HandlerFunc`), it may participate in both kinds of +implementation queries (by method-sets and function signatures). +Queries using method-sets should be invoked on the type or method name, +and queries using signatures should be invoked on a `func` or `(` token. + Client support: - **VS Code**: Use [Go to Implementations](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-implementation) (`⌘F12`). - **Emacs + eglot**: Use `M-x eglot-find-implementation`. diff --git a/gopls/doc/release/v0.19.0.md b/gopls/doc/release/v0.19.0.md index 18088732656..149a474244a 100644 --- a/gopls/doc/release/v0.19.0.md +++ b/gopls/doc/release/v0.19.0.md @@ -7,6 +7,33 @@ # New features +## "Implementations" supports signature types + +The Implementations query reports the correspondence between abstract +and concrete types and their methods based on their method sets. +Now, it also reports the correspondence between function types, +dynamic function calls, and function definitions, based on their signatures. + +To use it, invoke an Implementations query on the `func` token of the +definition of a named function, named method, or function literal. +Gopls reports the set of function signature types that abstract this +function, and the set of dynamic calls through values of such types. + +Conversely, an Implementations query on the `func` token of a +signature type, or on the `(` paren of a dynamic function call, +reports the set of concrete functions that the signature abstracts +or that the call dispatches to. + +Since a type may be both a function type and a named type with methods +(for example, `http.HandlerFunc`), it may participate in both kinds of +Implements queries (method-sets and function signatures). +Queries using method-sets should be invoked on the type or method name, +and queries using signatures should be invoked on a `func` or `(` token. + +Only the local (same-package) algorithm is currently supported. +TODO: implement global. + + ## "Eliminate dot import" code action This code action, available on a dotted import, will offer to replace diff --git a/gopls/internal/golang/implementation.go b/gopls/internal/golang/implementation.go index a7a7e663d44..2d9a1e93ef3 100644 --- a/gopls/internal/golang/implementation.go +++ b/gopls/internal/golang/implementation.go @@ -12,6 +12,7 @@ import ( "go/token" "go/types" "reflect" + "slices" "sort" "strings" "sync" @@ -21,10 +22,13 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/methodsets" + "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/safetoken" + "golang.org/x/tools/internal/astutil/cursor" + "golang.org/x/tools/internal/astutil/edge" "golang.org/x/tools/internal/event" ) @@ -74,9 +78,26 @@ func Implementation(ctx context.Context, snapshot *cache.Snapshot, f file.Handle } func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.Location, error) { - // First, find the object referenced at the cursor by type checking the - // current package. - obj, pkg, err := implementsObj(ctx, snapshot, fh.URI(), pp) + // Type check the current package. + pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) + if err != nil { + return nil, err + } + pos, err := pgf.PositionPos(pp) + if err != nil { + return nil, err + } + + // Find implementations based on func signatures. + if locs, err := implFuncs(pkg, pgf, pos); err != errNotHandled { + return locs, err + } + + // Find implementations based on method sets. + + // First, find the object referenced at the cursor. + // The object may be declared in a different package. + obj, err := implementsObj(pkg, pgf, pos) if err != nil { return nil, err } @@ -272,21 +293,9 @@ func offsetToLocation(ctx context.Context, snapshot *cache.Snapshot, filename st return m.OffsetLocation(start, end) } -// implementsObj returns the object to query for implementations, which is a -// type name or method. -// -// The returned Package is the narrowest package containing ppos, which is the -// package using the resulting obj but not necessarily the declaring package. -func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, ppos protocol.Position) (types.Object, *cache.Package, error) { - pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, uri) - if err != nil { - return nil, nil, err - } - pos, err := pgf.PositionPos(ppos) - if err != nil { - return nil, nil, err - } - +// implementsObj returns the object to query for implementations, +// which is a type name or method. +func implementsObj(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (types.Object, error) { // This function inherits the limitation of its predecessor in // requiring the selection to be an identifier (of a type or // method). But there's no fundamental reason why one could @@ -299,11 +308,11 @@ func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.D // TODO(adonovan): simplify: use objectsAt? path := pathEnclosingObjNode(pgf.File, pos) if path == nil { - return nil, nil, ErrNoIdentFound + return nil, ErrNoIdentFound } id, ok := path[0].(*ast.Ident) if !ok { - return nil, nil, ErrNoIdentFound + return nil, ErrNoIdentFound } // Is the object a type or method? Reject other kinds. @@ -319,17 +328,18 @@ func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.D // ok case *types.Func: if obj.Signature().Recv() == nil { - return nil, nil, fmt.Errorf("%s is a function, not a method", id.Name) + return nil, fmt.Errorf("%s is a function, not a method (query at 'func' token to find matching signatures)", id.Name) } case nil: - return nil, nil, fmt.Errorf("%s denotes unknown object", id.Name) + return nil, fmt.Errorf("%s denotes unknown object", id.Name) default: // e.g. *types.Var -> "var". kind := strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types.")) - return nil, nil, fmt.Errorf("%s is a %s, not a type", id.Name, kind) + // TODO(adonovan): improve upon "nil is a nil, not a type". + return nil, fmt.Errorf("%s is a %s, not a type", id.Name, kind) } - return obj, pkg, nil + return obj, nil } // localImplementations searches within pkg for declarations of all @@ -679,9 +689,236 @@ func pathEnclosingObjNode(f *ast.File, pos token.Pos) []ast.Node { } // Reverse path so leaf is first element. - for i := 0; i < len(path)/2; i++ { - path[i], path[len(path)-1-i] = path[len(path)-1-i], path[i] - } + slices.Reverse(path) return path } + +// --- Implementations based on signature types -- + +// implFuncs finds Implementations based on func types. +// +// Just as an interface type abstracts a set of concrete methods, a +// function type abstracts a set of concrete functions. Gopls provides +// analogous operations for navigating from abstract to concrete and +// back in the domain of function types. +// +// A single type (for example http.HandlerFunc) can have both an +// underlying type of function (types.Signature) and have methods that +// cause it to implement an interface. To avoid a confusing user +// interface we want to separate the two operations so that the user +// can unambiguously specify the query they want. +// +// So, whereas Implementations queries on interface types are usually +// keyed by an identifier of a named type, Implementations queries on +// function types are keyed by the "func" keyword, or by the "(" of a +// call expression. The query relates two sets of locations: +// +// 1. the "func" token of each function declaration (FuncDecl or +// FuncLit). These are analogous to declarations of concrete +// methods. +// +// 2. uses of abstract functions: +// +// (a) the "func" token of each FuncType that is not part of +// Func{Decl,Lit}. These are analogous to interface{...} types. +// +// (b) the "(" paren of each dynamic call on a value of an +// abstract function type. These are analogous to references to +// interface method names, but no names are involved, which has +// historically made them hard to search for. +// +// An Implementations query on a location in set 1 returns set 2, +// and vice versa. +// +// implFuncs returns errNotHandled to indicate that we should try the +// regular method-sets algorithm. +func implFuncs(pkg *cache.Package, pgf *parsego.File, pos token.Pos) ([]protocol.Location, error) { + curSel, ok := pgf.Cursor.FindPos(pos, pos) + if !ok { + return nil, fmt.Errorf("no code selected") + } + + info := pkg.TypesInfo() + + // Find innermost enclosing FuncType or CallExpr. + // + // We are looking for specific tokens (FuncType.Func and + // CallExpr.Lparen), but FindPos prefers an adjoining + // subexpression: given f(x) without additional spaces between + // tokens, FindPos always returns either f or x, never the + // CallExpr itself. Thus we must ascend the tree. + // + // Another subtlety: due to an edge case in go/ast, FindPos at + // FuncDecl.Type.Func does not return FuncDecl.Type, only the + // FuncDecl, because the orders of tree positions and tokens + // are inconsistent. Consequently, the ancestors for a "func" + // token of Func{Lit,Decl} do not include FuncType, hence the + // explicit cases below. + for _, cur := range curSel.Stack(nil) { + switch n := cur.Node().(type) { + case *ast.FuncDecl, *ast.FuncLit: + if inToken(n.Pos(), "func", pos) { + // Case 1: concrete function declaration. + // Report uses of corresponding function types. + switch n := n.(type) { + case *ast.FuncDecl: + return funcUses(pkg, info.Defs[n.Name].Type()) + case *ast.FuncLit: + return funcUses(pkg, info.TypeOf(n.Type)) + } + } + + case *ast.FuncType: + if n.Func.IsValid() && inToken(n.Func, "func", pos) && !beneathFuncDef(cur) { + // Case 2a: function type. + // Report declarations of corresponding concrete functions. + return funcDefs(pkg, info.TypeOf(n)) + } + + case *ast.CallExpr: + if inToken(n.Lparen, "(", pos) { + t := dynamicFuncCallType(info, n) + if t == nil { + return nil, fmt.Errorf("not a dynamic function call") + } + // Case 2b: dynamic call of function value. + // Report declarations of corresponding concrete functions. + return funcDefs(pkg, t) + } + } + } + + // It's probably a query of a named type or method. + // Fall back to the method-sets computation. + return nil, errNotHandled +} + +var errNotHandled = errors.New("not handled") + +// funcUses returns all locations in the workspace that are dynamic +// uses of the specified function type. +func funcUses(pkg *cache.Package, t types.Type) ([]protocol.Location, error) { + var locs []protocol.Location + + // local search + for _, pgf := range pkg.CompiledGoFiles() { + for cur := range pgf.Cursor.Preorder((*ast.CallExpr)(nil), (*ast.FuncType)(nil)) { + var pos, end token.Pos + var ftyp types.Type + switch n := cur.Node().(type) { + case *ast.CallExpr: + ftyp = dynamicFuncCallType(pkg.TypesInfo(), n) + pos, end = n.Lparen, n.Lparen+token.Pos(len("(")) + + case *ast.FuncType: + if !beneathFuncDef(cur) { + // func type (not def) + ftyp = pkg.TypesInfo().TypeOf(n) + pos, end = n.Func, n.Func+token.Pos(len("func")) + } + } + if ftyp == nil { + continue // missing type information + } + if unify(t, ftyp) { + loc, err := pgf.PosLocation(pos, end) + if err != nil { + return nil, err + } + locs = append(locs, loc) + } + } + } + + // TODO(adonovan): implement global search + + return locs, nil +} + +// funcDefs returns all locations in the workspace that define +// functions of the specified type. +func funcDefs(pkg *cache.Package, t types.Type) ([]protocol.Location, error) { + var locs []protocol.Location + + // local search + for _, pgf := range pkg.CompiledGoFiles() { + for curFn := range pgf.Cursor.Preorder((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) { + fn := curFn.Node() + var ftyp types.Type + switch fn := fn.(type) { + case *ast.FuncDecl: + ftyp = pkg.TypesInfo().Defs[fn.Name].Type() + case *ast.FuncLit: + ftyp = pkg.TypesInfo().TypeOf(fn) + } + if ftyp == nil { + continue // missing type information + } + if unify(t, ftyp) { + pos := fn.Pos() + loc, err := pgf.PosLocation(pos, pos+token.Pos(len("func"))) + if err != nil { + return nil, err + } + locs = append(locs, loc) + } + } + } + + // TODO(adonovan): implement global search, by analogy with + // methodsets algorithm. + // + // One optimization: if any signature type has free package + // names, look for matches only in packages among the rdeps of + // those packages. + + return locs, nil +} + +// beneathFuncDef reports whether the specified FuncType cursor is a +// child of Func{Decl,Lit}. +func beneathFuncDef(cur cursor.Cursor) bool { + ek, _ := cur.Edge() + switch ek { + case edge.FuncDecl_Type, edge.FuncLit_Type: + return true + } + return false +} + +// dynamicFuncCallType reports whether call is a dynamic (non-method) function call. +// If so, it returns the function type, otherwise nil. +// +// Tested via ../test/marker/testdata/implementation/signature.txt. +func dynamicFuncCallType(info *types.Info, call *ast.CallExpr) types.Type { + fun := ast.Unparen(call.Fun) + tv := info.Types[fun] + + // Reject conversion, or call to built-in. + if !tv.IsValue() { + return nil + } + + // Reject call to named func/method. + if id, ok := fun.(*ast.Ident); ok && is[*types.Func](info.Uses[id]) { + return nil + } + + // Reject method selections (T.method() or x.method()) + if sel, ok := fun.(*ast.SelectorExpr); ok { + seln, ok := info.Selections[sel] + if !ok || seln.Kind() != types.FieldVal { + return nil + } + } + + // TODO(adonovan): consider x() where x : TypeParam. + return tv.Type.Underlying() // e.g. x() or x.field() +} + +// inToken reports whether pos is within the token of +// the specified position and string. +func inToken(tokPos token.Pos, tokStr string, pos token.Pos) bool { + return tokPos <= pos && pos <= tokPos+token.Pos(len(tokStr)) +} diff --git a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go index dff8dfa109f..2fc3e042061 100644 --- a/gopls/internal/test/marker/doc.go +++ b/gopls/internal/test/marker/doc.go @@ -212,9 +212,10 @@ Here is the list of supported action markers: - hovererr(src, sm stringMatcher): performs a textDocument/hover at the src location, and checks that the error matches the given stringMatcher. - - implementations(src location, want ...location): makes a - textDocument/implementation query at the src location and - checks that the resulting set of locations matches want. + - implementation(src location, want ...location, err=stringMatcher): + makes a textDocument/implementation query at the src location and + checks that the resulting set of locations matches want. If err is + set, the implementation query must fail with the expected error. - incomingcalls(src location, want ...location): makes a callHierarchy/incomingCalls query at the src location, and checks that diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index a3e62d35968..3ff7da65ac5 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -584,7 +584,7 @@ var actionMarkerFuncs = map[string]func(marker){ "highlightall": actionMarkerFunc(highlightAllMarker), "hover": actionMarkerFunc(hoverMarker), "hovererr": actionMarkerFunc(hoverErrMarker), - "implementation": actionMarkerFunc(implementationMarker), + "implementation": actionMarkerFunc(implementationMarker, "err"), "incomingcalls": actionMarkerFunc(incomingCallsMarker), "inlayhints": actionMarkerFunc(inlayhintsMarker), "outgoingcalls": actionMarkerFunc(outgoingCallsMarker), @@ -2375,13 +2375,19 @@ func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) { // implementationMarker implements the @implementation marker. func implementationMarker(mark marker, src protocol.Location, want ...protocol.Location) { + wantErr := namedArgFunc(mark, "err", convertStringMatcher, stringMatcher{}) + got, err := mark.server().Implementation(mark.ctx(), &protocol.ImplementationParams{ TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), }) - if err != nil { + if err != nil && wantErr.empty() { mark.errorf("implementation at %s failed: %v", src, err) return } + if !wantErr.empty() { + wantErr.checkErr(mark, err) + return + } if err := compareLocations(mark, got, want); err != nil { mark.errorf("implementation: %v", err) } diff --git a/gopls/internal/test/marker/testdata/implementation/signature.txt b/gopls/internal/test/marker/testdata/implementation/signature.txt new file mode 100644 index 00000000000..b94d048a135 --- /dev/null +++ b/gopls/internal/test/marker/testdata/implementation/signature.txt @@ -0,0 +1,79 @@ +Test of local Implementation queries using function signatures. + +Assertions: +- Query on "func" of a function type returns the corresponding concrete functions. +- Query on "func" of a concrete function returns corresponding function types. +- Query on "(" of a dynamic function call returns corresponding function types. +- Different signatures (Nullary vs Handler) don't correspond. + +The @loc markers use the suffixes Func, Type, Call for the three kinds. +Each query maps between these two sets: {Func} <=> {Type,Call}. + +-- go.mod -- +module example.com +go 1.18 + +-- a/a.go -- +package a + +// R is short for Record. +type R struct{} + +// H is short for Handler. +type H func(*R) //@ loc(HType, "func"), implementation("func", aFunc, bFunc, cFunc) + +func aFunc(*R) {} //@ loc(aFunc, "func"), implementation("func", HType, hParamType, hCall) + +var bFunc = func(*R) {} //@ loc(bFunc, "func"), implementation("func", hParamType, hCall, HType) + +func nullary() { //@ loc(nullaryFunc, "func"), implementation("func", Nullary, fieldCall) + cFunc := func(*R) {} //@ loc(cFunc, "func"), implementation("func", hParamType, hCall, HType) + _ = cFunc +} + +type Nullary func() //@ loc(Nullary, "func") + +func _( + h func(*R)) { //@ loc(hParamType, "func"), implementation("func", aFunc, bFunc, cFunc) + + _ = aFunc // pacify unusedfunc + _ = nullary // pacify unusedfunc + _ = h + + h(nil) //@ loc(hCall, "("), implementation("(", aFunc, bFunc, cFunc) +} + +// generics: + +func _[T any](complex128) { + f1 := func(T) int { return 0 } //@ loc(f1Func, "func"), implementation("func", fParamType, fCall, f1Call, f2Call) + f2 := func(string) int { return 0 } //@ loc(f2Func, "func"), implementation("func", fParamType, fCall, f1Call, f2Call) + f3 := func(int) int { return 0 } //@ loc(f3Func, "func"), implementation("func", f1Call) + + f1(*new(T)) //@ loc(f1Call, "("), implementation("(", f1Func, f2Func, f3Func, f4Func) + f2("") //@ loc(f2Call, "("), implementation("(", f1Func, f2Func, f4Func) + _ = f3 // not called +} + +func f4[T any](T) int { return 0 } //@ loc(f4Func, "func"), implementation("func", fParamType, fCall, f1Call, f2Call) + +var _ = f4[string] // pacify unusedfunc + +func _( + f func(string) int, //@ loc(fParamType, "func"), implementation("func", f1Func, f2Func, f4Func) + err error) { + + f("") //@ loc(fCall, "("), implementation("(", f1Func, f2Func, f4Func) + + struct{x Nullary}{}.x() //@ loc(fieldCall, "("), implementation("(", nullaryFunc) + + // Calls that are not dynamic function calls: + _ = len("") //@ implementation("(", err="not a dynamic function call") + _ = int(0) //@ implementation("(", err="not a dynamic function call") + _ = error.Error(nil) //@ implementation("(", err="not a dynamic function call") + _ = err.Error() //@ implementation("(", err="not a dynamic function call") + _ = f4(0) //@ implementation("(", err="not a dynamic function call"), loc(f4Call, "(") +} + + + From 6a5b66bef78dc7a1cf8593b276f35102ec0cb11c Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Wed, 5 Mar 2025 11:56:25 -0800 Subject: [PATCH 99/99] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I13ce38cd00119b55ee384af53f27a72feb72572b Reviewed-on: https://go-review.googlesource.com/c/tools/+/655020 Reviewed-by: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase Auto-Submit: Gopher Robot --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- gopls/go.mod | 8 ++++---- gopls/go.sum | 22 +++++++++++----------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index bc7636b4cf8..3a120629b94 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ go 1.23.0 require ( github.com/google/go-cmp v0.6.0 github.com/yuin/goldmark v1.4.13 - golang.org/x/mod v0.23.0 - golang.org/x/net v0.35.0 - golang.org/x/sync v0.11.0 + golang.org/x/mod v0.24.0 + golang.org/x/net v0.37.0 + golang.org/x/sync v0.12.0 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 ) -require golang.org/x/sys v0.30.0 // indirect +require golang.org/x/sys v0.31.0 // indirect diff --git a/go.sum b/go.sum index 2d11b060c08..3d0337c8351 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.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= diff --git a/gopls/go.mod b/gopls/go.mod index 210943206b8..da7303222d2 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -5,11 +5,11 @@ go 1.24.0 require ( github.com/google/go-cmp v0.6.0 github.com/jba/templatecheck v0.7.1 - golang.org/x/mod v0.23.0 - golang.org/x/sync v0.11.0 - golang.org/x/sys v0.30.0 + golang.org/x/mod v0.24.0 + golang.org/x/sync v0.12.0 + golang.org/x/sys v0.31.0 golang.org/x/telemetry v0.0.0-20250220152412-165e2f84edbc - golang.org/x/text v0.22.0 + golang.org/x/text v0.23.0 golang.org/x/tools v0.30.0 golang.org/x/vuln v1.1.4 gopkg.in/yaml.v3 v3.0.1 diff --git a/gopls/go.sum b/gopls/go.sum index ef93b2c4601..20633541388 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -16,36 +16,36 @@ github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGK github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa h1:Br3+0EZZohShrmVVc85znGpxw7Ca8hsUJlrdT/JQGw8= golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 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.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/telemetry v0.0.0-20250220152412-165e2f84edbc h1:HS+G1Mhh2dxM8ObutfYKdjfD7zpkyeP/UxeRnJpIZtQ= golang.org/x/telemetry v0.0.0-20250220152412-165e2f84edbc/go.mod h1:bDzXkYUaHzz51CtDy5kh/jR4lgPxsdbqC37kp/dzhCc= 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.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 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= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=