This rolls up to include CL 780780, preparing us for the freeze.

Change-Id: I69561591332e377fddac20af99d85cf92f8bc2cf
Reviewed-on: https://go-review.googlesource.com/c/go/+/780801
TryBot-Bypass: Mark Freeman <markfreeman@google.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
This commit is contained in:
Mark Freeman 2026-05-20 17:05:26 -04:00
parent f571fc93b0
commit bbf60f3bbd
34 changed files with 1429 additions and 587 deletions

View file

@ -9,9 +9,9 @@ require (
golang.org/x/mod v0.36.1-0.20260513122029-343ee60345a1
golang.org/x/sync v0.20.0
golang.org/x/sys v0.44.0
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6
golang.org/x/term v0.39.0
golang.org/x/tools v0.44.1-0.20260414062052-55fb96ff894f
golang.org/x/tools v0.45.1-0.20260520205638-b38156a7a9f5
)
require (

View file

@ -16,13 +16,13 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4=
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.44.1-0.20260414062052-55fb96ff894f h1:OsDhJTPRMdqueEUhZ6K1sdC07K6rj9i4RYTQGF6zSHA=
golang.org/x/tools v0.44.1-0.20260414062052-55fb96ff894f/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.45.1-0.20260520205638-b38156a7a9f5 h1:jqdNq3qAaJT9zQL5Cbq/TRYEdoLZmystI2hoCyAsAuw=
golang.org/x/tools v0.45.1-0.20260520205638-b38156a7a9f5/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef h1:mqLYrXCXYEZOop9/Dbo6RPX11539nwiCNBb1icVPmw8=
rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef/go.mod h1:8xcPgWmwlZONN1D9bjxtHEjrUtSEa3fakVF8iaewYKQ=

View file

@ -10,6 +10,7 @@ import (
"fmt"
"go/ast"
"go/types"
"slices"
"strings"
"golang.org/x/tools/go/analysis"
@ -49,108 +50,88 @@ func init() {
Analyzer.Flags.BoolVar(&whitelist, "whitelist", whitelist, "use composite white list; for testing only")
}
// runUnkeyedLiteral checks if a composite literal is a struct literal with
// unkeyed fields.
func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.CompositeLit)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
cl := n.(*ast.CompositeLit)
for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) {
complit := curLit.Node().(*ast.CompositeLit)
typ := pass.TypesInfo.Types[cl].Type
if typ == nil {
// cannot determine composite literals' type, skip it
return
// Skip empty or partly/fully keyed literals.
if len(complit.Elts) == 0 ||
slices.ContainsFunc(complit.Elts, func(e ast.Expr) bool { return is[*ast.KeyValueExpr](e) }) {
continue
}
// Find struct type.
// (For a type parameter, choose an arbitrary term.)
typ := pass.TypesInfo.Types[complit].Type
if typ == nil {
continue // no type info
}
terms, err := typeparams.NormalTerms(typ)
if err != nil || len(terms) == 0 {
continue // invalid or empty type
}
t := terms[0].Type()
strct, ok := typeparams.Deref(t).Underlying().(*types.Struct)
if !ok {
continue // not a struct literal
}
if isSamePackageType(pass, t) {
continue // allow unkeyed literals for structs in same package
}
// Allow whitelisted types.
typeName := typ.String()
if whitelist && unkeyedLiteral[typeName] {
// skip whitelisted types
return
}
var structuralTypes []types.Type
switch typ := types.Unalias(typ).(type) {
case *types.TypeParam:
terms, err := typeparams.StructuralTerms(typ)
if err != nil {
return // invalid type
}
for _, term := range terms {
structuralTypes = append(structuralTypes, term.Type())
}
default:
structuralTypes = append(structuralTypes, typ)
continue
}
for _, typ := range structuralTypes {
strct, ok := typeparams.Deref(typ).Underlying().(*types.Struct)
if !ok {
// skip non-struct composite literals
continue
}
if isLocalType(pass, typ) {
// allow unkeyed locally defined composite literal
continue
}
// check if the struct contains an unkeyed field
allKeyValue := true
var suggestedFixAvailable = len(cl.Elts) == strct.NumFields()
var missingKeys []analysis.TextEdit
for i, e := range cl.Elts {
if _, ok := e.(*ast.KeyValueExpr); !ok {
allKeyValue = false
if i >= strct.NumFields() {
break
}
field := strct.Field(i)
if !field.Exported() {
// Adding unexported field names for structs not defined
// locally will not work.
suggestedFixAvailable = false
break
}
missingKeys = append(missingKeys, analysis.TextEdit{
Pos: e.Pos(),
End: e.Pos(),
NewText: fmt.Appendf(nil, "%s: ", field.Name()),
})
// If there is one value per field,
// offer to fill in the field names.
var fixes []analysis.SuggestedFix
if len(complit.Elts) == strct.NumFields() {
var edits []analysis.TextEdit
for i, elt := range complit.Elts {
field := strct.Field(i)
// We cannot fill in the name of an
// exported field from another package.
if !field.Exported() {
edits = nil
break
}
edits = append(edits, analysis.TextEdit{
Pos: elt.Pos(),
End: elt.Pos(),
NewText: fmt.Appendf(nil, "%s: ", field.Name()),
})
}
if allKeyValue {
// all the struct fields are keyed
continue
}
diag := analysis.Diagnostic{
Pos: cl.Pos(),
End: cl.End(),
Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName),
}
if suggestedFixAvailable {
diag.SuggestedFixes = []analysis.SuggestedFix{{
if edits != nil {
fixes = []analysis.SuggestedFix{{
Message: "Add field names to struct literal",
TextEdits: missingKeys,
TextEdits: edits,
}}
}
pass.Report(diag)
return
}
})
pass.Report(analysis.Diagnostic{
Pos: complit.Pos(),
End: complit.End(),
Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName),
SuggestedFixes: fixes,
})
}
return nil, nil
}
// isLocalType reports whether typ belongs to the same package as pass.
// TODO(adonovan): local means "internal to a function"; rename to isSamePackageType.
func isLocalType(pass *analysis.Pass, typ types.Type) bool {
// isSamePackageType reports whether typ belongs to the same package as pass.
func isSamePackageType(pass *analysis.Pass, typ types.Type) bool {
switch x := types.Unalias(typ).(type) {
case *types.Struct:
// struct literals are local types
return true
case *types.Pointer:
return isLocalType(pass, x.Elem())
return isSamePackageType(pass, x.Elem())
case interface{ Obj() *types.TypeName }: // *Named or *TypeParam (aliases were removed already)
// names in package foo are local to foo_test too
return x.Obj().Pkg() != nil &&
@ -158,3 +139,8 @@ func isLocalType(pass *analysis.Pass, typ types.Type) bool {
}
return false
}
func is[T any](x any) bool {
_, ok := x.(T)
return ok
}

View file

@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The errorsas package defines an Analyzer that checks that the second argument to
// errors.As is a pointer to a type implementing error.
// Package errorsas defines an Analyzer that checks that the second argument to
// [errors.As] is a pointer to a type implementing error.
package errorsas
import (
@ -19,7 +19,12 @@ import (
const Doc = `report passing non-pointer or non-error values to errors.As
The errorsas analyzer reports calls to errors.As where the type
of the second argument is not a pointer to a type implementing error.`
of the second argument is not a pointer to a type implementing error.
For example:
var unwrappedErr net.DNSError
errors.As(err, unwrappedErr) // should use &unwrappedErr, DNSError.Error has a pointer receiver
`
var Analyzer = &analysis.Analyzer{
Name: "errorsas",

View file

@ -61,6 +61,12 @@ inliner machinery is capable of replacing f by a function literal,
func(){...}(). However, the inline analyzer discards all such
"literalizations" unconditionally, again on grounds of style.)
A call to a function F from its dedicated test (TestF) is not inlined,
since the purpose of the test is to exercise F itself, even when
it's a deprecated function to which other calls should be inlined.
This is not true for type aliases; see https://go.dev/issue/79271.
See further discussion in https://go.dev/issue/79272.
## Constants
Given a constant that is marked for inlining, like this one:

View file

@ -339,23 +339,19 @@ func (a *analyzer) inlineAlias(tn *types.TypeName, curId inspector.Cursor) {
expr = curId.Node().(*ast.IndexListExpr)
}
fieldType := curId
if fieldType.ParentEdgeKind() == edge.StarExpr_X {
fieldType = fieldType.Parent()
}
if fieldType.ParentEdgeKind() == edge.Field_Type {
field := fieldType.Parent().Node().(*ast.Field)
if len(field.Names) == 0 {
identicalName := false
if rhs, ok := alias.Rhs().(*types.Named); ok {
identicalName = alias.Obj().Name() == rhs.Obj().Name()
}
if !identicalName {
// Type is embedded, inlining the alias will cause
// the field name to be changed, which might break
// programs in terms of backwards compatibility.
return
}
// Reject inlining of a type alias used to declare an embedded
// struct field if doing so would change the field's name.
if v, ok := a.pass.TypesInfo.Defs[id].(*types.Var); ok && v.Embedded() {
identicalName := false
// TODO(adonovan): should we allow a pointer (type A = *pkg.A)?
if rhs, ok := alias.Rhs().(*types.Named); ok {
identicalName = alias.Obj().Name() == rhs.Obj().Name()
}
if !identicalName {
// Type is embedded, inlining the alias will cause
// the field name to be changed, which might break
// programs in terms of backwards compatibility.
return
}
}

View file

@ -19,14 +19,13 @@ import (
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var atomicTypesAnalyzer = &analysis.Analyzer{
var AtomicTypesAnalyzer = &analysis.Analyzer{
Name: "atomictypes",
Doc: analyzerutil.MustExtractDoc(doc, "atomictypes"),
Requires: []*analysis.Analyzer{
@ -37,11 +36,6 @@ var atomicTypesAnalyzer = &analysis.Analyzer{
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#atomictypes",
}
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.AtomicTypesModernizer = atomicTypesAnalyzer
}
// TODO(mkalil): support the Pointer variants.
// Consider the following function signatures for pointer loading:
// func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

View file

@ -111,6 +111,31 @@ The any analyzer suggests replacing uses of the empty interface type,
`interface{}`, with the `any` alias, which was introduced in Go 1.18.
This is a purely stylistic change that makes code more readable.
# Analyzer embedlit
embedlit: simplify references to embedded fields in composite literals
The embedlit analyzer suggests removing redundant embedded field type specifiers
from composite literals. Go1.27 introduced the ability to directly initialize
fields promoted from embedded struct types without a nested literal. For
example, given the following structs:
type T struct {
U
}
type U struct {
x int
}
A composite literal such as
t := T{U: U{x: 1}}
would become
t := T{x: 1}
# Analyzer errorsastype
errorsastype: replace errors.As with errors.AsType[T]
@ -142,6 +167,9 @@ The fmtappendf analyzer suggests replacing `[]byte(fmt.Sprintf(...))` with
by Sprintf, making the code more efficient. The suggestion also applies to
fmt.Sprint and fmt.Sprintln.
Since its fix is not a Pareto improvement, fmtappendf is disabled by default in
the `go fix` analyzer suite; see golang/go#77581.
# Analyzer forvar
forvar: remove redundant re-declaration of loop variables
@ -424,6 +452,16 @@ It also handles variants using [strings.IndexByte] instead of Index, or the byte
Fixes are offered only in cases in which there are no potential modifications of the idx, s, or substr expressions between their definition and use.
It also replaces [strings.SplitN](s, sep, 2)[0] and [strings.Split](s, sep)[0] with the "before" result of strings.Cut, when sep is a non-empty string constant:
x := strings.SplitN(s, sep, 2)[0]
is replaced by:
x, _, _ := strings.Cut(s, sep)
The fix is only offered when sep is a non-empty string literal. When sep is a variable or the empty string, the semantics differ (strings.Split(s, "")[0] returns the first character of s, but strings.Cut(s, "").before is ""), so no fix is suggested.
# Analyzer stringscutprefix
stringscutprefix: replace HasPrefix/TrimPrefix with CutPrefix
@ -506,6 +544,9 @@ is replaced by:
This avoids quadratic memory allocation and improves performance.
No diagnostics are issued in tests, where data sizes are often
small and asymptotic performance is not a security concern.
The analyzer requires that all references to s before the final uses
are += operations. To avoid warning about trivial cases, at least one
must appear within a loop. The variable s must be a local

View file

@ -0,0 +1,406 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"bytes"
"fmt"
"go/ast"
"go/token"
"go/types"
"slices"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/moreiters"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var EmbedLitAnalyzer = &analysis.Analyzer{
Name: "embedlit",
Doc: analyzerutil.MustExtractDoc(doc, "embedlit"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: runEmbedLit,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#embedlit",
}
// Go1.27 introduced the ability to directly access embedded struct fields.
// The embedlit modernizer suggests two types of fixes that use this feature:
// 1. Removing redundant field type specifiers in embedded struct fields.
// 2. Moving embedded struct field assignments inside of the struct literal
// initialization.
func runEmbedLit(pass *analysis.Pass) (any, error) {
var (
inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
)
for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) {
if curLit.ParentEdgeKind() != edge.KeyValueExpr_Value { // non-nested comp lit
// TODO(mkalil): Figure out how to handle addition/removal of commas in
// the comp lit when we observe code where both patterns apply. (This will
// likely require a significant amount of work). For now, only apply edits
// from one pattern at a time.
if !embedlitUnnest(pass, info, curLit) {
err := embedlitCombine(pass, index, info, curLit) // calls pass.ReadFile
if err != nil {
return nil, err
}
}
}
}
return nil, nil
}
// Pattern A: removing unneeded embedded field type specifier from the struct
// literal.
// T{U: U{f: v, ...}} => T{f: v, ...}
// It returns true if it reported a diagnostic with edits.
func embedlitUnnest(pass *analysis.Pass, info *types.Info, curLit inspector.Cursor) bool {
var (
edits []analysis.TextEdit
names []string // names of the embedded field types that can be removed
lit = curLit.Node().(*ast.CompositeLit)
compLitType = info.TypeOf(lit)
)
// checkLit determines whether any of the fields in the given struct literal can
// be promoted, and calculates the corresponding edits.
var checkLit func(lit *ast.CompositeLit)
checkLit = func(lit *ast.CompositeLit) {
for _, elt := range lit.Elts {
// Can't promote an unkeyed field; would result in a syntax error.
if kv, ok := elt.(*ast.KeyValueExpr); ok {
if innerLit := isEmbeddedFieldLit(info, compLitType, kv); innerLit != nil {
// Emit edits to delete the unnecessary embedded field type specifier
// and its closing brace.
closingPos := innerLit.Rbrace
if len(innerLit.Elts) > 0 {
// Delete any inner trailing commas or white space. Extra trailing commas
// would result in invalid code.
closingPos = innerLit.Elts[len(innerLit.Elts)-1].End()
}
file := astutil.EnclosingFile(curLit)
// Enable modernizer only for Go1.27.
if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) {
return
}
// If any comments overlap with the range to delete, don't suggest a fix.
if !moreiters.Empty(astutil.Comments(file, kv.Pos(), innerLit.Lbrace+1)) ||
!moreiters.Empty(astutil.Comments(file, closingPos, innerLit.Rbrace+1)) {
continue
}
edits = append(edits, []analysis.TextEdit{
// T{U: U{f: v, ...}}
// ----- -
{
// Delete the key and the opening brace of the inner struct literal.
Pos: kv.Pos(),
End: innerLit.Lbrace + 1,
},
{
// Delete the corresponding closing brace, including preceding
// white space or commas. Failing to delete trailing commas may
// result in invalid code.
Pos: closingPos,
End: innerLit.Rbrace + 1,
},
}...)
names = append(names, kv.Key.(*ast.Ident).Name)
checkLit(innerLit)
}
}
}
}
checkLit(lit)
if len(edits) > 0 {
pass.Report(analysis.Diagnostic{
Pos: curLit.Node().Pos(),
End: curLit.Node().End(),
Message: "embedded field type can be removed from struct literal",
SuggestedFixes: []analysis.SuggestedFix{
{
Message: fmt.Sprintf("Remove embedded field type%s %s", cond(len(names) == 1, "", "s"), strings.Join(names, ", ")),
TextEdits: edits,
},
},
})
return true
}
return false
}
// Pattern B: moving embedded field assignments inside the struct literal
// initialization.
// t := T{...}; t.x = x => t := T{..., x: x}
// (or var t = ...)
func embedlitCombine(pass *analysis.Pass, index *typeindex.Index, info *types.Info, curLit inspector.Cursor) error {
compLit := curLit.Node().(*ast.CompositeLit)
if !moreiters.Every(slices.Values(compLit.Elts), func(e ast.Expr) bool {
return is[*ast.KeyValueExpr](e)
}) {
// Promoting additional embedded fields would result in mixing keyed and
// unkeyed fields, which isn't allowed.
return nil
}
var (
// Ident for "t" in the assignment.
lhs *ast.Ident
// The cursor representing the statement that initializes the comp lit "t".
// We use its siblings to search for field assignments and verify that there
// are no intervening statements, in case those statements observe "t".
curStmt inspector.Cursor
)
switch curLit.ParentEdgeKind() {
case edge.AssignStmt_Rhs:
assign := curLit.Parent().Node().(*ast.AssignStmt)
// TODO(mkalil): Handle lhs forms that aren't idents, i.e. x.y[i] = T{...}.
if id, ok := assign.Lhs[curLit.ParentEdgeIndex()].(*ast.Ident); ok {
lhs = id
curStmt = curLit.Parent()
}
case edge.ValueSpec_Values:
spec := curLit.Parent().Node().(*ast.ValueSpec)
lhs = spec.Names[curLit.ParentEdgeIndex()]
if decl, ok := moreiters.First(curLit.Enclosing((*ast.DeclStmt)(nil))); ok {
curStmt = decl
}
default:
return nil
}
if lhs == nil || !curStmt.Valid() {
return nil
}
var (
tObj = info.ObjectOf(lhs)
// Marks the contiguous block of embedded field assign statements that will
// be moved into the struct initialization.
firstStmt, lastStmt inspector.Cursor
)
stmtloop:
for {
var ok bool
curStmt, ok = curStmt.NextSibling()
if !ok {
break // end of (e.g.) block
}
// All embedded field value assignments must immediately follow the struct
// initialization.
assign, ok := curStmt.Node().(*ast.AssignStmt)
if !ok || len(assign.Lhs) != 1 || !(assign.Tok == token.ASSIGN || assign.Tok == token.DEFINE) {
// TODO(mkalil): handle multi-assignments like t.x, t.y = 1, 2
break
}
expr := assign.Lhs[0]
sel, ok := expr.(*ast.SelectorExpr)
if !ok {
break
}
// Verify that sel.X refers to the same object as "t"
selXId, ok := sel.X.(*ast.Ident)
if !ok {
// TODO(mkalil): handle deeply nested expressions like t.B.x
break
}
obj := info.ObjectOf(selXId)
if obj != tObj {
break
}
rhsCur := curStmt.ChildAt(edge.AssignStmt_Rhs, 0)
if uses(index, rhsCur, tObj) {
break
}
for c := range rhsCur.Preorder((*ast.Ident)(nil)) {
id := c.Node().(*ast.Ident)
// If the rhs uses a value of t (e.g. t.x = t.y), don't suggest a fix because
// we can't evaluate t.y when constructing the new literal.
if info.ObjectOf(id) == tObj {
break stmtloop
}
// Note: we don't need to worry about expressions with side effects
// changing the behavior when moved inside the comp lit. The order of
// effects will be preserved because we preserve the order of the key
// value pairs inside the comp lit.
}
if !firstStmt.Valid() {
firstStmt = curStmt
}
lastStmt = curStmt
}
if !firstStmt.Valid() {
return nil
}
file := astutil.EnclosingFile(curLit)
// Enable modernizer only for Go1.27.
if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) {
return nil
}
// Read file content to determine if the struct lit has a trailing comma
// after its last element.
tokFile := pass.Fset.File(compLit.Rbrace)
filename := tokFile.Name()
src, err := pass.ReadFile(filename)
if err != nil {
return err
}
hasTrailingComma := false
if len(compLit.Elts) > 0 {
lastElt := compLit.Elts[len(compLit.Elts)-1]
lastEltOffset := tokFile.Offset(lastElt.End())
rbraceOffset := tokFile.Offset(compLit.Rbrace)
hasTrailingComma = bytes.Contains(src[lastEltOffset:rbraceOffset], []byte(","))
}
var edits []analysis.TextEdit
// Emit edits to move the field assignment into the struct lit while
// removing it from its current place.
// t := T{...}; t.x = v
// ----- --- -
// t := T{..., x: v}
// Add a trailing comma before the closing brace of compLit if one doesn't
// exist, and delete the closing brace itself.
// t := T{...}; t.x = v
// -
// t := T{..., t.x = v
if len(compLit.Elts) > 0 && !hasTrailingComma {
edits = append(edits, analysis.TextEdit{
Pos: compLit.Rbrace,
End: compLit.Rbrace + 1,
NewText: []byte(","),
})
} else {
edits = append(edits, analysis.TextEdit{
Pos: compLit.Rbrace,
End: compLit.Rbrace + 1,
})
}
// For each assignment:
// t.x = v
// -- ---
// x : v
curStmt = firstStmt
var prevStmt inspector.Cursor
for {
assign := curStmt.Node().(*ast.AssignStmt)
expr := assign.Lhs[0]
sel := expr.(*ast.SelectorExpr)
// Delete "t."
edits = append(edits, analysis.TextEdit{
Pos: assign.Pos(),
End: sel.Sel.Pos(),
})
// Replace "=" with ":"
edits = append(edits, analysis.TextEdit{
Pos: expr.End(),
End: assign.TokPos + 1,
NewText: []byte(":"),
})
// Add a comma after the previous assignment if this is not the first one.
if prevStmt.Valid() {
edits = append(edits, analysis.TextEdit{
Pos: prevStmt.Node().End(),
NewText: []byte(","),
})
}
// For the last assignment, add the closing brace of the struct lit.
if curStmt == lastStmt {
edits = append(edits, analysis.TextEdit{
Pos: assign.End(),
NewText: []byte("}"),
})
break
}
prevStmt = curStmt
curStmt, _ = curStmt.NextSibling() // can't fail because we break out of the loop when we hit lastStmt
}
pass.Report(analysis.Diagnostic{
Pos: curLit.Node().Pos(),
End: curLit.Node().End(),
Message: "embedded field assignment can be moved to struct literal",
SuggestedFixes: []analysis.SuggestedFix{
{
Message: "Move embedded field assignment to struct literal",
TextEdits: edits,
},
},
})
return nil
}
// isEmbeddedFieldLit determines whether elt is a KeyValueExpr "T: T{...}" for
// an embedded field for which we can safely remove its type.
// If so, it returns the corresponding CompositeLit.
// If elt contains an unkeyed field or ambiguous type, it returns nil.
func isEmbeddedFieldLit(info *types.Info, topLevelType types.Type, kv *ast.KeyValueExpr) *ast.CompositeLit {
obj := keyedField(info, kv)
if obj == nil || !obj.Embedded() {
return nil
}
lit, ok := kv.Value.(*ast.CompositeLit)
if !ok || len(lit.Elts) == 0 {
// Skip if the struct literal is empty.
return nil
}
// We cannot remove this type if any of its nested composite elements have
// unkeyed fields or are ambiguous, so we check for those conditions before
// returning.
for _, elt := range lit.Elts {
kv, ok := elt.(*ast.KeyValueExpr)
if !ok {
return nil
}
obj := keyedField(info, kv)
if obj == nil {
return nil
}
k := kv.Key.(*ast.Ident) // can't fail
// Cannot promote an ambiguous type, for example:
// type T struct { A; B }
// type A struct { x int }
// type B struct { x int }
// _ = T{A: A{x: 1}}
// cannot be simplified to T{x: 1} because T has two different embedded fields called "x".
// We also reject composite literals with slice elements, as parentObj will be nil.
parentObj, _, _ := types.LookupFieldOrMethod(topLevelType, true, obj.Pkg(), k.Name)
if parentObj != obj {
return nil
}
}
return lit
}
// keyedField reports whether the key of kv is an embedded field type. If so, it
// returns the type of the embedded field, otherwise it returns nil.
func keyedField(info *types.Info, kv *ast.KeyValueExpr) *types.Var {
k, ok := kv.Key.(*ast.Ident)
if !ok {
return nil
}
obj, ok := info.ObjectOf(k).(*types.Var)
if !ok || !obj.IsField() {
return nil
}
return obj
}

View file

@ -17,14 +17,14 @@ import (
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/moreiters"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var errorsastypeAnalyzer = &analysis.Analyzer{
var ErrorsAsTypeAnalyzer = &analysis.Analyzer{
Name: "errorsastype",
Doc: analyzerutil.MustExtractDoc(doc, "errorsastype"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#errorsastype",
@ -32,11 +32,6 @@ var errorsastypeAnalyzer = &analysis.Analyzer{
Run: errorsastype,
}
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.ErrorsAsTypeModernizer = errorsastypeAnalyzer
}
// errorsastype offers a fix to replace error.As with the newer
// errors.AsType[T] following this pattern:
//
@ -60,22 +55,15 @@ func init() {
//
// because the transformation in that case would be ungainly.
//
// For the negated case (!errors.As), we use !ok instead.
//
// Note that the cmd/vet suite includes the "errorsas" analyzer, which
// detects actual mistakes in the use of errors.As. This logic does
// not belong in errorsas because the problems it fixes are merely
// stylistic.
//
// TODO(adonovan): support more cases:
//
// - Negative cases
// var myerr E
// if !errors.As(err, &myerr) { ... }
// =>
// myerr, ok := errors.AsType[E](err)
// if !ok { ... }
//
// - if myerr := new(E); errors.As(err, myerr); { ... }
//
// - if errors.As(err, myerr) && othercond { ... }
func errorsastype(pass *analysis.Pass) (any, error) {
var (
@ -89,7 +77,7 @@ func errorsastype(pass *analysis.Pass) (any, error) {
continue // spread call: errors.As(pair())
}
v, curDeclStmt := canUseErrorsAsType(info, index, curCall)
v, curDeclStmt, curIfStmt := canUseErrorsAsType(info, index, curCall)
if v == nil {
continue
}
@ -127,64 +115,82 @@ func errorsastype(pass *analysis.Pass) (any, error) {
// Choose a name for the "ok" variable.
// We generate a new name only if 'ok' is already declared at
// curCall and it also used within the if-statement.
curIf := curCall.Parent()
ifScope := info.Scopes[curIf.Node().(*ast.IfStmt)]
okName := freshName(info, index, ifScope, v.Pos(), curCall, curIf, token.NoPos, "ok")
ifScope := info.Scopes[curIfStmt.Node().(*ast.IfStmt)]
negated := curCall.ParentEdgeKind() == edge.UnaryExpr_X // bool => Tok==NOT
okName := freshName(info, index, ifScope, v.Pos(), curCall, curIfStmt, token.NoPos, "ok")
// Because we reject any use of v outside the if statement, any use besides
// the argument in errors.As must lie inside the if statement.
usesV := moreiters.Len(index.Uses(v)) > 1
edits := append(
// delete "var myerr *MyErr"
refactor.DeleteStmt(pass.Fset.File(call.Fun.Pos()), curDeclStmt),
// if errors.As (err, &myerr) { ... }
// ------------- -------------- -------- ----
// if myerr, ok := errors.AsType[*MyErr](err ); ok { ... }
analysis.TextEdit{
// Insert "myerr, ok := " if myerr is used inside the if statement.
// Otherwise insert "_, ok := ".
Pos: call.Pos(),
End: call.Pos(),
NewText: fmt.Appendf(nil, "%s, %s := ", cond(usesV, v.Name(), "_"), okName),
},
analysis.TextEdit{
// replace As with AsType[T]
Pos: asIdent.Pos(),
End: asIdent.End(),
NewText: fmt.Appendf(nil, "AsType[%s]", errtype),
},
analysis.TextEdit{
// delete ", &myerr"
Pos: call.Args[0].End(),
End: call.Args[1].End(),
},
analysis.TextEdit{
// insert "; ok" for errors.AsType or "; !ok" for !errors.AsType
Pos: call.End(),
End: call.End(),
NewText: fmt.Appendf(nil, "; %s%s", cond(negated, "!", ""), okName),
},
)
if negated {
unaryExpr := curCall.Parent().Node().(*ast.UnaryExpr)
// delete "!"
edits = append(edits, analysis.TextEdit{
Pos: unaryExpr.OpPos,
End: unaryExpr.X.Pos(),
})
}
pass.Report(analysis.Diagnostic{
Pos: call.Fun.Pos(),
End: call.Fun.End(),
Message: fmt.Sprintf("errors.As can be simplified using AsType[%s]", errtype),
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Replace errors.As with AsType[%s]", errtype),
TextEdits: append(
// delete "var myerr *MyErr"
refactor.DeleteStmt(pass.Fset.File(call.Fun.Pos()), curDeclStmt),
// if errors.As (err, &myerr) { ... }
// ------------- -------------- -------- ----
// if myerr, ok := errors.AsType[*MyErr](err ); ok { ... }
analysis.TextEdit{
// insert "myerr, ok := "
Pos: call.Pos(),
End: call.Pos(),
NewText: fmt.Appendf(nil, "%s, %s := ", v.Name(), okName),
},
analysis.TextEdit{
// replace As with AsType[T]
Pos: asIdent.Pos(),
End: asIdent.End(),
NewText: fmt.Appendf(nil, "AsType[%s]", errtype),
},
analysis.TextEdit{
// delete ", &myerr"
Pos: call.Args[0].End(),
End: call.Args[1].End(),
},
analysis.TextEdit{
// insert "; ok"
Pos: call.End(),
End: call.End(),
NewText: fmt.Appendf(nil, "; %s", okName),
},
),
Message: fmt.Sprintf("Replace errors.As with AsType[%s]", errtype),
TextEdits: edits,
}},
})
}
return nil, nil
}
// canUseErrorsAsType reports whether curCall is a call to
// errors.As beneath an if statement, preceded by a
// declaration of the typed error var. The var must not be
// used outside the if statement.
func canUseErrorsAsType(info *types.Info, index *typeindex.Index, curCall inspector.Cursor) (_ *types.Var, _ inspector.Cursor) {
if curCall.ParentEdgeKind() != edge.IfStmt_Cond {
return // not beneath if statement
// canUseErrorsAsType reports whether curCall is a call to errors.As beneath an
// if statement, preceded by a declaration of the typed error var. The var must
// not be used outside the if statement.
// If the conditions are met, it returns the error var, the cursor for its
// DeclStmt, and the cursor for the IfStmt that contains the call to errors.As.
// Otherwise it returns a nil error var.
func canUseErrorsAsType(info *types.Info, index *typeindex.Index, curCall inspector.Cursor) (_ *types.Var, curDeclStmt, curIfStmt inspector.Cursor) {
curCond := curCall
if curCond.ParentEdgeKind() == edge.UnaryExpr_X { // if !errors.As(err, &v)
curCond = curCond.Parent()
}
var (
curIfStmt = curCall.Parent()
ifStmt = curIfStmt.Node().(*ast.IfStmt)
)
if curCond.ParentEdgeKind() != edge.IfStmt_Cond {
return // not beneath if or unaryexpr
}
curIfStmt = curCond.Parent()
ifStmt := curIfStmt.Node().(*ast.IfStmt)
if ifStmt.Init != nil {
return // if statement already has an init part
}
@ -228,5 +234,5 @@ func canUseErrorsAsType(info *types.Info, index *typeindex.Index, curCall inspec
// ...
// if errors.As(err, &v) { ... }
// with no uses of v outside the IfStmt.
return v, curDecl.Parent() // DeclStmt
return v, curDecl.Parent(), curIfStmt // curDecl.Parent() is a DeclStmt
}

View file

@ -19,6 +19,7 @@ import (
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
@ -230,6 +231,7 @@ func minmax(pass *analysis.Pass) (any, error) {
if compare, ok := ifStmt.Cond.(*ast.BinaryExpr); ok &&
ifStmt.Init == nil &&
isInequality(compare.Op) != 0 &&
typesinternal.NoEffects(info, compare) &&
isAssignBlock(ifStmt.Body) {
// a blank var has no type.
if tLHS := info.TypeOf(ifStmt.Body.List[0].(*ast.AssignStmt).Lhs[0]); tLHS != nil && !maybeNaN(tLHS) {
@ -361,18 +363,18 @@ func canUseBuiltinMinMax(fn *types.Func, body *ast.BlockStmt) bool {
return false
}
return hasMinMaxLogic(body, fn.Name())
return hasMinMaxLogic(body, fn.Name(), sig.Params().At(0).Name(), sig.Params().At(1).Name())
}
// hasMinMaxLogic checks if the function body implements simple min/max logic.
func hasMinMaxLogic(body *ast.BlockStmt, funcName string) bool {
func hasMinMaxLogic(body *ast.BlockStmt, funcName, param0, param1 string) bool {
// Pattern 1: Single if/else statement
if len(body.List) == 1 {
if ifStmt, ok := body.List[0].(*ast.IfStmt); ok {
// Get the "false" result from the else block
if elseBlock, ok := ifStmt.Else.(*ast.BlockStmt); ok && len(elseBlock.List) == 1 {
if elseRet, ok := elseBlock.List[0].(*ast.ReturnStmt); ok && len(elseRet.Results) == 1 {
return checkMinMaxPattern(ifStmt, elseRet.Results[0], funcName)
return checkMinMaxPattern(ifStmt, elseRet.Results[0], funcName, param0, param1)
}
}
}
@ -382,7 +384,7 @@ func hasMinMaxLogic(body *ast.BlockStmt, funcName string) bool {
if len(body.List) == 2 {
if ifStmt, ok := body.List[0].(*ast.IfStmt); ok && ifStmt.Else == nil {
if retStmt, ok := body.List[1].(*ast.ReturnStmt); ok && len(retStmt.Results) == 1 {
return checkMinMaxPattern(ifStmt, retStmt.Results[0], funcName)
return checkMinMaxPattern(ifStmt, retStmt.Results[0], funcName, param0, param1)
}
}
}
@ -394,7 +396,8 @@ func hasMinMaxLogic(body *ast.BlockStmt, funcName string) bool {
// ifStmt: the if statement to check
// falseResult: the expression returned when the condition is false
// funcName: "min" or "max"
func checkMinMaxPattern(ifStmt *ast.IfStmt, falseResult ast.Expr, funcName string) bool {
// param0, param1: the two parameter names for the function.
func checkMinMaxPattern(ifStmt *ast.IfStmt, falseResult ast.Expr, funcName, param0, param1 string) bool {
// Must have condition with comparison
cmp, ok := ifStmt.Cond.(*ast.BinaryExpr)
if !ok {
@ -417,10 +420,24 @@ func checkMinMaxPattern(ifStmt *ast.IfStmt, falseResult ast.Expr, funcName strin
return false // Not a comparison operator
}
t := thenRet.Results[0] // "true" result
f := falseResult // "false" result
x := cmp.X // left operand
y := cmp.Y // right operand
t := thenRet.Results[0] // "true" result
f := falseResult // "false" result
x, ok := cmp.X.(*ast.Ident) // left operand
if !ok {
return false // Not a basic min/max comparison
}
y, ok := cmp.Y.(*ast.Ident) // right operand
if !ok {
return false // Not a basic min/max comparison
}
// Check that the min max algorithm uses the function's params
// Which param corresponds to which part of the operation doesn't matter,
// so we have to try both.
if !(param0 == x.Name && param1 == y.Name ||
param0 == y.Name && param1 == x.Name) {
return false
}
// Check operand order and adjust sign accordingly
if astutil.EqualSyntax(t, x) && astutil.EqualSyntax(f, y) {

View file

@ -17,7 +17,6 @@ import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/refactor"
@ -35,24 +34,26 @@ var doc string
// Suite lists all modernize analyzers.
var Suite = []*analysis.Analyzer{
AnyAnalyzer,
atomicTypesAnalyzer,
AtomicTypesAnalyzer,
// AppendClippedAnalyzer, // not nil-preserving!
// BLoopAnalyzer, // may skew benchmark results, see golang/go#74967
FmtAppendfAnalyzer,
EmbedLitAnalyzer,
ErrorsAsTypeAnalyzer,
// FmtAppendfAnalyzer, // makes code less clear, see golang/go#77581
ForVarAnalyzer,
MapsLoopAnalyzer,
MinMaxAnalyzer,
NewExprAnalyzer,
OmitZeroAnalyzer,
plusBuildAnalyzer,
PlusBuildAnalyzer,
RangeIntAnalyzer,
ReflectTypeForAnalyzer,
slicesbackwardAnalyzer,
slicesBackwardAnalyzer,
SlicesContainsAnalyzer,
// SlicesDeleteAnalyzer, // not nil-preserving!
SlicesSortAnalyzer,
stditeratorsAnalyzer,
stringscutAnalyzer,
StdIteratorsAnalyzer,
StringsCutAnalyzer,
StringsCutPrefixAnalyzer,
StringsSeqAnalyzer,
StringsBuilderAnalyzer,
@ -121,15 +122,6 @@ func within(pass *analysis.Pass, pkgs ...string) bool {
moreiters.Contains(stdlib.Dependencies(pkgs...), path)
}
// unparenEnclosing removes enclosing parens from cur in
// preparation for a call to [Cursor.ParentEdge].
func unparenEnclosing(cur inspector.Cursor) inspector.Cursor {
for cur.ParentEdgeKind() == edge.ParenExpr_X {
cur = cur.Parent()
}
return cur
}
var (
builtinAny = types.Universe.Lookup("any")
builtinAppend = types.Universe.Lookup("append")

View file

@ -11,22 +11,16 @@ import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/versions"
)
var plusBuildAnalyzer = &analysis.Analyzer{
var PlusBuildAnalyzer = &analysis.Analyzer{
Name: "plusbuild",
Doc: analyzerutil.MustExtractDoc(doc, "plusbuild"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#plusbuild",
Run: plusbuild,
}
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.PlusBuildModernizer = plusBuildAnalyzer
}
func plusbuild(pass *analysis.Pass) (any, error) {
check := func(f *ast.File) {
// "//go:build" directives were added in go1.17, but

View file

@ -356,16 +356,9 @@ func isScalarLvalue(info *types.Info, curId inspector.Cursor) bool {
// as it is always true for a variable even when that variable is
// used only as an r-value. So we must inspect enclosing syntax.
cur := curId
cur := astutil.UnparenEnclosingCursor(curId)
// Strip enclosing parens.
ek := cur.ParentEdgeKind()
for ek == edge.ParenExpr_X {
cur = cur.Parent()
ek = cur.ParentEdgeKind()
}
switch ek {
switch cur.ParentEdgeKind() {
case edge.AssignStmt_Lhs:
assign := cur.Parent().Node().(*ast.AssignStmt)
if assign.Tok != token.DEFINE {

View file

@ -65,9 +65,9 @@ func reflecttypefor(pass *analysis.Pass) (any, error) {
// Special cases for TypeOf((*T)(nil)).Elem(), and
// TypeOf([]T(nil)).Elem(), needed when T is an interface type.
if curCall.ParentEdgeKind() == edge.SelectorExpr_X {
curSel := unparenEnclosing(curCall).Parent()
curSel := astutil.UnparenEnclosingCursor(curCall).Parent()
if curSel.ParentEdgeKind() == edge.CallExpr_Fun {
call2 := unparenEnclosing(curSel).Parent().Node().(*ast.CallExpr) // potentially .Elem()
call2 := astutil.UnparenEnclosingCursor(curSel).Parent().Node().(*ast.CallExpr) // potentially .Elem()
obj := typeutil.Callee(info, call2)
if typesinternal.IsMethodNamed(obj, "reflect", "Type", "Elem") {
// reflect.TypeOf(expr).Elem()

View file

@ -9,6 +9,7 @@ import (
"go/ast"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
@ -23,7 +24,7 @@ import (
"golang.org/x/tools/internal/versions"
)
var slicesbackwardAnalyzer = &analysis.Analyzer{
var slicesBackwardAnalyzer = &analysis.Analyzer{
Name: "slicesbackward",
Doc: analyzerutil.MustExtractDoc(doc, "slicesbackward"),
Requires: []*analysis.Analyzer{
@ -36,7 +37,7 @@ var slicesbackwardAnalyzer = &analysis.Analyzer{
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.SlicesBackwardModernizer = slicesbackwardAnalyzer
goplsexport.SlicesBackwardModernizer = slicesBackwardAnalyzer
}
// slicesbackward offers a fix to replace a manually-written backward loop:
@ -147,18 +148,47 @@ func slicesbackward(pass *analysis.Pass) (any, error) {
// s[i] — pure element accesses that can be replaced by the value var
// other — index used for non-indexing purposes
var (
sliceIndexes []*ast.IndexExpr
otherUses int
// First assignment in the loop body of the form "name := s[i]"; or nil.
firstSliceIdxAssign *ast.AssignStmt
// List of s[i] expressions to replace by the value var (excludes firstSliceIdxAssign, which will be entirely removed).
sliceIdxsReplace []*ast.IndexExpr
// Total count of s[i] usages.
sliceIdxs int
// Non-indexing uses of i.
otherUses int
)
for curUse := range index.Uses(indexObj) {
if !bodyCur.Contains(curUse) {
continue
}
// Is i in the Index position of an s[i] expression?
// If so, we also need to check whether s[i] is an lvalue. If we're
// mutating the slice or taking an element's address, a fix will not
// be offered.
if curUse.ParentEdgeKind() == edge.IndexExpr_Index {
idxExpr := curUse.Parent().Node().(*ast.IndexExpr)
if isScalarLvalue(pass.TypesInfo, curUse.Parent()) {
continue nextLoop
}
idxCur := curUse.Parent()
idxExpr := idxCur.Node().(*ast.IndexExpr)
if astutil.EqualSyntax(idxExpr.X, sliceExpr) {
sliceIndexes = append(sliceIndexes, idxExpr)
sliceIdxs++
// If the current statement is the first in the body of the form
// "name := s[i]", save it so we can use "name" as the value
// variable in slices.Backward. We can also remove the entire assign
// statement.
if firstSliceIdxAssign == nil && idxCur.ParentEdgeKind() == edge.AssignStmt_Rhs {
assignStmt := idxCur.Parent().Node().(*ast.AssignStmt)
if len(assignStmt.Lhs) == 1 && assignStmt.Tok == token.DEFINE {
// The condition above implies that assignStmt.Lhs[0] is a valid
// identifier.
firstSliceIdxAssign = assignStmt
// We don't need to replace the index expr with the value variable
// name if we are going to remove the entire assignment.
continue
}
}
sliceIdxsReplace = append(sliceIdxsReplace, idxExpr)
continue
}
}
@ -167,15 +197,18 @@ func slicesbackward(pass *analysis.Pass) (any, error) {
// Build the suggested fix.
//
// for i := len(s) - 1; i >= 0; i-- { ... s[i] ... }
// ---------------------------- ----
// _, v := range slices.Backward(s) v
// for i := len(s) - 1; i >= 0; i-- { ... s[i] ... }
// -------------------------------- ----
// for _, v := range slices.Backward(s) { ... v ... }
sliceStr := astutil.Format(pass.Fset, sliceExpr)
prefix, edits := refactor.AddImport(info, file, "slices", "slices", "Backward", loop.Pos())
elemName := freshName(info, index, info.Scopes[loop], loop.Pos(), bodyCur, bodyCur, token.NoPos, "v")
elemName := chooseValueName(firstSliceIdxAssign, sliceStr)
elemName = freshName(info, index, info.Scopes[loop], loop.Pos(), bodyCur, bodyCur, token.NoPos, elemName)
// Replace each s[i] with elemName.
for _, sx := range sliceIndexes {
// Replace each s[i] with elemName (except for in the statement of the
// form "name := s[i]" where we might have gotten elemName from - we will
// delete this entire statement instead).
for _, sx := range sliceIdxsReplace {
edits = append(edits, analysis.TextEdit{
Pos: sx.Pos(),
End: sx.End(),
@ -183,17 +216,27 @@ func slicesbackward(pass *analysis.Pass) (any, error) {
})
}
// Replace the loop header with a range over slices.Backward.
var header string
if otherUses == 0 && len(sliceIndexes) > 0 {
// All uses of i are s[i]; drop the index variable.
header = fmt.Sprintf("_, %s := range %sBackward(%s)",
elemName, prefix, sliceStr)
} else {
// i is used for other purposes; keep both index and value.
header = fmt.Sprintf("%s, %s := range %sBackward(%s)",
indexIdent.Name, elemName, prefix, sliceStr)
if firstSliceIdxAssign != nil {
edits = append(edits, analysis.TextEdit{
Pos: firstSliceIdxAssign.Pos(),
End: firstSliceIdxAssign.End(),
})
}
// Replace the loop header with a range over slices.Backward. In
// well-typed code, at least one of the index or value variables must be
// referenced inside the loop body (otherUses + sliceIndexes > 0).
var vars string
if otherUses == 0 { // sliceIdxs > 0
// All uses of i are s[i]; drop the index variable.
vars = fmt.Sprintf("_, %s", elemName)
} else if sliceIdxs == 0 { // otherUses > 0
// Index i is not used in any s[i] expressions; drop the value variable.
vars = indexIdent.Name
} else { // otherUses > 0 && sliceIdxs > 0, keep both variables.
vars = fmt.Sprintf("%s, %s", indexIdent.Name, elemName)
}
header := fmt.Sprintf("%s := range %sBackward(%s)", vars, prefix, sliceStr)
edits = append(edits, analysis.TextEdit{
Pos: loop.Init.Pos(),
End: loop.Post.End(),
@ -213,3 +256,20 @@ func slicesbackward(pass *analysis.Pass) (any, error) {
}
return nil, nil
}
// chooseValueName uses a heuristic to generate a name for the value variable in
// the call to slices.Backward.
func chooseValueName(assign *ast.AssignStmt, sliceStr string) string {
if assign != nil {
return assign.Lhs[0].(*ast.Ident).Name
}
// Heuristic: remove plural s suffix from slice var
// if present, otherwise use first letter.
if token.IsIdentifier(sliceStr) && len(sliceStr) > 1 {
if single, ok := strings.CutSuffix(sliceStr, "s"); ok {
return single
}
return sliceStr[:1] // first letter (assuming ASCII)
}
return "v"
}

View file

@ -19,6 +19,7 @@ import (
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
@ -58,12 +59,8 @@ var SlicesContainsAnalyzer = &analysis.Analyzer{
// statement is "found = false" (or vice versa), the
// loop becomes "found = [!]slices.Contains(...)".
//
// It may change cardinality of effects of the "needle" expression.
// (Mostly this appears to be a desirable optimization, avoiding
// redundantly repeated evaluation.)
//
// TODO(adonovan): Add a check that needle/predicate expression from
// if-statement has no effects. Now the program behavior may change.
// It rejects candidates whose needle/predicate expression from the if-statement
// has side effects to avoid changes in program behavior.
func slicescontains(pass *analysis.Pass) (any, error) {
// Skip the analyzer in packages where its
// fixes would create an import cycle.
@ -174,6 +171,11 @@ func slicescontains(pass *analysis.Pass) (any, error) {
return
}
// Reject if needle/predicate expression has side effects.
if !typesinternal.NoEffects(info, arg2) {
return
}
// Reject if the body, needle or predicate references either range variable.
usesRangeVar := func(n ast.Node) bool {
cur, ok := curRange.FindNode(n)

View file

@ -17,12 +17,11 @@ import (
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/stdlib"
"golang.org/x/tools/internal/typesinternal/typeindex"
)
var stditeratorsAnalyzer = &analysis.Analyzer{
var StdIteratorsAnalyzer = &analysis.Analyzer{
Name: "stditerators",
Doc: analyzerutil.MustExtractDoc(doc, "stditerators"),
Requires: []*analysis.Analyzer{
@ -32,11 +31,6 @@ var stditeratorsAnalyzer = &analysis.Analyzer{
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stditerators",
}
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.StdIteratorsModernizer = stditeratorsAnalyzer
}
// stditeratorsTable records std types that have legacy T.{Len,At}
// iteration methods as well as a newer T.All method that returns an
// iter.Seq.

View file

@ -13,6 +13,7 @@ import (
"go/types"
"maps"
"slices"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
@ -47,6 +48,7 @@ func stringsbuilder(pass *analysis.Pass) (any, error) {
var (
inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
)
// Gather all local string variables that appear on the
@ -55,7 +57,7 @@ func stringsbuilder(pass *analysis.Pass) (any, error) {
for curAssign := range inspect.Root().Preorder((*ast.AssignStmt)(nil)) {
assign := curAssign.Node().(*ast.AssignStmt)
if assign.Tok == token.ADD_ASSIGN && is[*ast.Ident](assign.Lhs[0]) {
if v, ok := pass.TypesInfo.Uses[assign.Lhs[0].(*ast.Ident)].(*types.Var); ok &&
if v, ok := info.Uses[assign.Lhs[0].(*ast.Ident)].(*types.Var); ok &&
v.Kind() == types.LocalVar &&
types.Identical(v.Type(), builtinString.Type()) {
candidates[v] = true
@ -75,7 +77,7 @@ func stringsbuilder(pass *analysis.Pass) (any, error) {
// Now check each candidate variable's decl and uses.
nextcand:
for _, v := range slices.SortedFunc(maps.Keys(candidates), lexicalOrder) {
var edits []analysis.TextEdit
var edits, postEdits []analysis.TextEdit // postEdits are emitted last
// Check declaration of s has one of these forms:
//
@ -100,6 +102,13 @@ nextcand:
if file == lastEditFile && v.Pos() < lastEditEnd {
continue
}
filename := pass.Fset.File(file.FileStart).Name()
// Suppress diagnostics in test files, where suggested fixes may increase
// verbosity, and performance doesn't matter as much.
// See https://go.dev/issue/78613
if strings.HasSuffix(filename, "_test.go") {
continue
}
ek := def.ParentEdgeKind()
if ek == edge.AssignStmt_Lhs &&
@ -121,10 +130,10 @@ nextcand:
// Add strings import.
prefix, importEdits := refactor.AddImport(
pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
info, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
edits = append(edits, importEdits...)
if isEmptyString(pass.TypesInfo, assign.Rhs[0]) {
if isEmptyString(info, assign.Rhs[0]) {
// s := ""
// ---------------------
// var s strings.Builder
@ -171,7 +180,7 @@ nextcand:
// Add strings import.
prefix, importEdits := refactor.AddImport(
pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
info, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
edits = append(edits, importEdits...)
spec := def.Parent().Node().(*ast.ValueSpec)
@ -193,7 +202,7 @@ nextcand:
NewText: fmt.Appendf(nil, " %sBuilder", prefix),
})
if len(spec.Values) > 0 && !isEmptyString(pass.TypesInfo, spec.Values[0]) {
if len(spec.Values) > 0 && !isEmptyString(info, spec.Values[0]) {
if decl.Rparen.IsValid() {
// var decl with explicit parens:
//
@ -273,11 +282,8 @@ nextcand:
)
for curUse := range index.Uses(v) {
// Strip enclosing parens around Ident.
curUse = astutil.UnparenEnclosingCursor(curUse)
ek := curUse.ParentEdgeKind()
for ek == edge.ParenExpr_X {
curUse = curUse.Parent()
ek = curUse.ParentEdgeKind()
}
// intervening reports whether cur has an ancestor of
// one of the given types that is within the scope of v.
@ -315,20 +321,21 @@ nextcand:
// s += expr
// ------------- -
// s.WriteString(expr)
edits = append(edits, []analysis.TextEdit{
edits = append(edits, analysis.TextEdit{
// replace " += " with ".WriteString("
{
Pos: assign.Lhs[0].End(),
End: assign.Rhs[0].Pos(),
NewText: []byte(".WriteString("),
},
Pos: assign.Lhs[0].End(),
End: assign.Rhs[0].Pos(),
NewText: []byte(".WriteString("),
})
// Delay inserting the closing parenthesis, in case it overlaps with a
// .String() edit, since it would need to come after.
postEdits = append(postEdits, analysis.TextEdit{
// insert ")"
{
Pos: assign.End(),
End: assign.End(),
NewText: []byte(")"),
},
}...)
Pos: assign.End(),
End: assign.End(),
NewText: []byte(")"),
})
} else if ek == edge.UnaryExpr_X &&
curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
@ -357,6 +364,8 @@ nextcand:
continue nextcand // no += in a loop; reject
}
edits = append(edits, postEdits...)
lastEditFile = file
lastEditEnd = edits[len(edits)-1].End

View file

@ -21,14 +21,13 @@ import (
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/moreiters"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var stringscutAnalyzer = &analysis.Analyzer{
var StringsCutAnalyzer = &analysis.Analyzer{
Name: "stringscut",
Doc: analyzerutil.MustExtractDoc(doc, "stringscut"),
Requires: []*analysis.Analyzer{
@ -39,11 +38,6 @@ var stringscutAnalyzer = &analysis.Analyzer{
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscut",
}
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.StringsCutModernizer = stringscutAnalyzer
}
// stringscut offers a fix to replace an occurrence of strings.Index{,Byte} with
// strings.{Cut,Contains}, and similar fixes for functions in the bytes package.
// Consider some candidate for replacement i := strings.Index(s, substr).
@ -92,7 +86,7 @@ func init() {
// }
//
// If the condition involving `i` is equivalent to i >= 0, then we replace it with
// `if ok.
// `if ok`.
// If the condition is negated (e.g. equivalent to `i < 0`), we use `if !ok` instead.
// If the slices of `s` match `s[:i]` or `s[i+len(substr):]` or their variants listed above,
// then we replace them with before and after.
@ -124,6 +118,8 @@ func stringscut(pass *analysis.Pass) (any, error) {
bytesIndexByte = index.Object("bytes", "IndexByte")
)
stringsplitCut(pass, index)
scopeFixCount := make(map[*types.Scope]int) // the number of times we have offered a fix within a given scope in the current pass
for _, obj := range []types.Object{
@ -149,11 +145,20 @@ func stringscut(pass *analysis.Pass) (any, error) {
switch ek, idx := curCall.ParentEdge(); ek {
case edge.ValueSpec_Values:
// Have: var i = strings.Index(...)
// If the call occurs in a multi-value declaration or assignment, don't suggest a fix because it would produce invalid code (See golang/go#78643).
spec := curCall.Parent().Node().(*ast.ValueSpec)
if len(spec.Names) != 1 {
continue
}
curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, idx)
iIdent = curName.Node().(*ast.Ident)
case edge.AssignStmt_Rhs:
// Have: i := strings.Index(...)
// (Must be i's definition.)
assign := curCall.Parent().Node().(*ast.AssignStmt)
if len(assign.Lhs) != 1 {
continue
}
curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, idx)
iIdent, _ = curLhs.Node().(*ast.Ident) // may be nil
}
@ -367,6 +372,129 @@ func stringscut(pass *analysis.Pass) (any, error) {
return nil, nil
}
// stringsplitCut reports patterns where strings.Split or strings.SplitN with
// n=2 is immediately indexed at [0], which can be simplified to strings.Cut,
// when sep is a non-empty string constant. The transformation is
// semantics-preserving only for non-empty sep: strings.Split(s, "")[0]
// returns the first character of s, but strings.Cut(s, "").before is "".
// For variable sep the value is unknown at analysis time, so we conservatively
// skip those cases too.
//
// For example:
//
// x := strings.SplitN(s, ",", 2)[0]
// ------ --
// x, _, _ := strings.Cut(s, ",")
//
// Requires Go 1.18 (when strings.Cut was added).
func stringsplitCut(pass *analysis.Pass, index *typeindex.Index) {
info := pass.TypesInfo
stringsSplit := index.Object("strings", "Split")
stringsSplitN := index.Object("strings", "SplitN")
for _, obj := range []types.Object{stringsSplit, stringsSplitN} {
for curCall := range index.Calls(obj) {
callExpr := curCall.Node().(*ast.CallExpr)
// For SplitN, the third argument must be the integer constant 2.
if obj.Name() == "SplitN" && !isIntLiteral(info, callExpr.Args[2], 2) {
continue
}
// Sep must be a non-empty constant string.
// strings.Split(s, "")[0] returns the first character of s, but
// strings.Cut(s, "").before is "", so the semantics differ for
// an empty sep. For a variable sep we cannot rule out "" at
// analysis time, so we conservatively skip those cases too.
sepTV := info.Types[callExpr.Args[1]]
if sepTV.Value == nil || constant.StringVal(sepTV.Value) == "" {
continue
}
// The call must be the X of an IndexExpr.
if curCall.ParentEdgeKind() != edge.IndexExpr_X {
continue
}
parent := curCall.Parent()
indexExpr := parent.Node().(*ast.IndexExpr)
// The index must be the integer constant 0.
if !isZeroIntConst(info, indexExpr.Index) {
continue
}
// The IndexExpr must be the sole RHS of an assignment statement.
if parent.ParentEdgeKind() != edge.AssignStmt_Rhs {
continue
}
assign := parent.Parent().Node().(*ast.AssignStmt)
if assign.Tok != token.DEFINE || len(assign.Lhs) != 1 {
continue
}
// The LHS must be a single non-blank identifier.
lhsIdent, ok := assign.Lhs[0].(*ast.Ident)
if !ok || lhsIdent.Name == "_" {
continue
}
// strings.Cut requires Go 1.18.
if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_18) {
continue
}
// Build the fix.
//
// x := strings.SplitN(s, sep, 2)[0]
// --- ------ ---
// x, _, _ := strings.Cut(s, sep)
callFunIdent := typesinternal.UsedIdent(info, callExpr.Fun)
var edits []analysis.TextEdit
// LHS: insert ", _, _" after x
edits = append(edits, analysis.TextEdit{
Pos: lhsIdent.End(),
End: lhsIdent.End(),
NewText: []byte(", _, _"),
})
// Function name: Split/SplitN → Cut
edits = append(edits, analysis.TextEdit{
Pos: callFunIdent.Pos(),
End: callFunIdent.End(),
NewText: []byte("Cut"),
})
// For SplitN: remove the ", 2" third argument.
if obj.Name() == "SplitN" {
edits = append(edits, analysis.TextEdit{
Pos: callExpr.Args[1].End(), // after sep
End: callExpr.Rparen, // before )
})
}
// Remove the "[0]" index expression.
edits = append(edits, analysis.TextEdit{
Pos: indexExpr.Lbrack,
End: indexExpr.End(),
})
pass.Report(analysis.Diagnostic{
Pos: callExpr.Fun.Pos(),
End: callExpr.Fun.End(),
Message: fmt.Sprintf("strings.%s call can be simplified using strings.Cut", obj.Name()),
Category: "stringscut",
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Simplify strings.%s call using strings.Cut", obj.Name()),
TextEdits: edits,
}},
})
}
}
}
// indexArgValid reports whether expr is a valid strings.Index(_, _) arg
// for the transformation. An arg is valid iff it is:
// - constant;
@ -387,10 +515,10 @@ func indexArgValid(info *types.Info, index *typeindex.Index, expr ast.Expr, afte
case *ast.Ident:
sObj := info.Uses[expr]
sUses := index.Uses(sObj)
return !hasModifyingUses(info, sUses, afterPos)
return !hasModifyingUses(sUses, afterPos)
default:
// For now, skip instances where s or substr are not
// identifers, basic lits, or call expressions of the form
// identifiers, basic lits, or call expressions of the form
// []byte(s).
// TODO(mkalil): Handle s and substr being expressions like ptr.field[i].
// From adonovan: We'd need to analyze s and substr to see
@ -487,18 +615,15 @@ func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr a
// hasModifyingUses reports whether any of the uses involve potential
// modifications. Uses involving assignments before the "afterPos" won't be
// considered.
func hasModifyingUses(info *types.Info, uses iter.Seq[inspector.Cursor], afterPos token.Pos) bool {
func hasModifyingUses(uses iter.Seq[inspector.Cursor], afterPos token.Pos) bool {
for curUse := range uses {
ek := curUse.ParentEdgeKind()
if ek == edge.AssignStmt_Lhs {
if curUse.Node().Pos() <= afterPos {
continue
}
assign := curUse.Parent().Node().(*ast.AssignStmt)
if sameObject(info, assign.Lhs[0], curUse.Node().(*ast.Ident)) {
// Modifying use because we are reassigning the value of the object.
return true
}
// Any use on the LHS is a modifying use.
return true
} else if ek == edge.UnaryExpr_X &&
curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
// Modifying use because we might be passing the object by reference (an explicit &).

View file

@ -416,7 +416,7 @@ func match(info *types.Info, arg ast.Expr, param *types.Var) bool {
}
// propagate propagates changes in wrapper (non-None) kind information backwards
// through through the wrapper.callers graph of well-formed forwarding calls.
// through the wrapper.callers graph of well-formed forwarding calls.
func propagate(pass *analysis.Pass, w *wrapper, call *ast.CallExpr, kind Kind, res *Result) {
// Check correct call forwarding.
//

View file

@ -12,7 +12,7 @@ import (
"reflect"
)
// A Kind describes a field of an ast.Node struct.
// A Kind describes a field of an [ast.Node] struct.
type Kind uint8
// String returns a description of the edge kind.
@ -41,21 +41,25 @@ func (k Kind) Get(n ast.Node, idx int) ast.Node {
panic(fmt.Sprintf("%v.Get(%T): invalid node type", k, n))
}
v := reflect.ValueOf(n).Elem().Field(fieldInfos[k].index)
if idx != -1 {
v = v.Index(idx) // asserts valid index
} else {
// (The type assertion below asserts that v is not a slice.)
if v.Kind() == reflect.Slice {
v = v.Index(idx) // asserts valid idx
} else if idx != -1 {
panic(fmt.Sprintf("%v, Get(%T, %d): cannot index non-slice", v, n, idx))
}
return v.Interface().(ast.Node) // may be nil
out, _ := v.Interface().(ast.Node) // may be nil
return out
}
// Each [Kind] is named Type_Field, where Type is the
// [ast.Node] struct type and Field is the name of the field
const (
Invalid Kind = iota // for nodes at the root of the traversal
// Kinds are sorted alphabetically.
// Numbering is not stable.
// Each is named Type_Field, where Type is the
// ast.Node struct type and Field is the name of the field
// As of Go1.26 these kinds are sorted alphabetically, but
// numbering must be stable, so any new addition of const should
// use a new value (be added at the end of the list).
ArrayType_Elt
ArrayType_Len

View file

@ -24,8 +24,10 @@
package objectpath
import (
"encoding/binary"
"fmt"
"go/types"
"slices"
"strconv"
"strings"
@ -124,7 +126,66 @@ func For(obj types.Object) (Path, error) {
// An Encoder amortizes the cost of encoding the paths of multiple objects.
// The zero value of an Encoder is ready to use.
type Encoder struct {
scopeMemo map[*types.Scope][]types.Object // memoization of scopeObjects
pkgIndex map[*types.Package]*pkgIndex
}
// A traversal encapsulates the state of a single traversal of the object/type graph.
type traversal struct {
pkg *types.Package
ix *pkgIndex // non-nil if we are building the index
target types.Object // the sought symbol (if ix == nil)
found Path // the found path (if ix == nil)
// These maps are used to short circuit cycles through
// interface methods, such as occur in the following example:
//
// type I interface { f() interface{I} }
//
// See golang/go#68046 for details.
seenTParamNames map[*types.TypeName]bool // global cycle breaking through type parameters
seenMethods map[*types.Func]bool // global cycle breaking through recursive interfaces
}
// A pkgIndex holds a compressed index of objectpaths of all symbols
// (fields, methods, params) requiring search for an entire package.
//
// The first time a search for a given package is requested, we simply
// traverse the type graph for the target object, maintaining the
// current object path as a stack. If we find the target object, we
// save the path and terminate the main loop (but it's not worth
// breaking out of the current recursion).
//
// On the second search (a pkgIndex exists but its data is nil), we
// build an index of the traversal, which we use for all subsequent
// searches.
//
// The traversal index is encoded in the data field as a list of records,
// one per node, in preorder. Records are of two types:
//
// - A record for a package-level object consists of a pair
// (parent, nameIndex uvarint), where parent is zero and
// nameIndex is the index of the object's name in the sorted
// pkg.Scope().Names() slice.
//
// - A record for a nested node (a segment of an object path)
// consists of (parent uvarint, op byte, index uvarint), where
// parent is the index of the record for the parent node,
// op is the destructuring operator, and index (if op = [AFMTr])
// is its integer operand.
//
// Since data[0] = 0 all nodes have positive offsets. In effect the
// encoding is a trie in which each node stores one path segment
// and points to the node for its prefix.
//
// TODO(adonovan): opt: evaluate an only 2-level tree with nodes for
// package-level objects and the-rest-of-the-path. One calculation
// suggested that it might be similar speed but 30% more compact.
type pkgIndex struct {
pkg *types.Package
data []byte // encoding of traversal; nil if not yet constructed
scopeNames []string // memo of pkg.Scope().Names() to avoid O(n) alloc/sort at lookup
offsets map[types.Object]uint32 // each object's node offset within encoded traversal data
}
// For returns the path to an object relative to its package,
@ -211,10 +272,9 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
if pkg == nil {
return "", fmt.Errorf("predeclared %s has no path", obj)
}
scope := pkg.Scope()
// 2. package-level object?
if scope.Lookup(obj.Name()) == obj {
if pkg.Scope().Lookup(obj.Name()) == obj {
// Only exported objects (and non-exported types) have a path.
// Non-exported types may be referenced by other objects.
if _, ok := obj.(*types.TypeName); !ok && !obj.Exported() {
@ -232,19 +292,18 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
// have a path.
return "", fmt.Errorf("no path for %v", obj)
}
case *types.Const, // Only package-level constants have a path.
*types.Label, // Labels are function-local.
*types.PkgName: // PkgNames are file-local.
return "", fmt.Errorf("no path for %v", obj)
case *types.Var:
// Could be:
// - a field (obj.IsField())
// - a func parameter or result
// - a local var.
// Sadly there is no way to distinguish
// a param/result from a local
// so we must proceed to the find.
// A var, if not package-level, must be a
// parameter (incl. receiver) or result, or a struct field.
if obj.Kind() == types.LocalVar {
return "", fmt.Errorf("no path for local %v", obj)
}
case *types.Func:
// A func, if not package-level, must be a method.
@ -261,89 +320,311 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
panic(obj)
}
// 4. Search the API for the path to the var (field/param/result) or method.
// 4. Search the object/type graph for the path to
// the var (field/param/result) or method.
ix, ok := enc.pkgIndex[pkg]
if !ok {
// First search: don't build an index, just traverse.
// This avoids allocation in [For], whose Encoder
// lives for a single call.
ix = &pkgIndex{pkg: pkg}
// First inspect package-level named types.
// In the presence of path aliases, these give
// the best paths because non-types may
// refer to types, but not the reverse.
empty := make([]byte, 0, 48) // initial space
objs := enc.scopeObjects(scope)
for _, o := range objs {
tname, ok := o.(*types.TypeName)
if !ok {
continue // handle non-types in second pass
if enc.pkgIndex == nil {
enc.pkgIndex = make(map[*types.Package]*pkgIndex)
}
enc.pkgIndex[pkg] = ix // build the index next time
f := traversal{pkg: pkg, target: obj}
f.traverse()
if f.found != "" {
return f.found, nil
}
} else {
// Second search: build an index while traversing.
if ix.data == nil {
ix.offsets = make(map[types.Object]uint32)
ix.data = []byte{0} // offset 0 is sentinel
(&traversal{pkg: pkg, ix: ix}).traverse()
}
path := append(empty, o.Name()...)
path = append(path, opType)
T := o.Type()
if alias, ok := T.(*types.Alias); ok {
if r := findTypeParam(obj, alias.TypeParams(), path, opTypeParam); r != nil {
return Path(r), nil
}
if r := find(obj, alias.Rhs(), append(path, opRhs)); r != nil {
return Path(r), nil
}
} else if tname.IsAlias() {
// legacy alias
if r := find(obj, T, path); r != nil {
return Path(r), nil
}
} else if named, ok := T.(*types.Named); ok {
// defined (named) type
if r := findTypeParam(obj, named.TypeParams(), path, opTypeParam); r != nil {
return Path(r), nil
}
if r := find(obj, named.Underlying(), append(path, opUnderlying)); r != nil {
return Path(r), nil
}
}
}
// Then inspect everything else:
// non-types, and declared methods of defined types.
for _, o := range objs {
path := append(empty, o.Name()...)
if _, ok := o.(*types.TypeName); !ok {
if o.Exported() {
// exported non-type (const, var, func)
if r := find(obj, o.Type(), append(path, opType)); r != nil {
return Path(r), nil
}
}
continue
}
// Inspect declared methods of defined types.
if T, ok := types.Unalias(o.Type()).(*types.Named); ok {
path = append(path, opType)
// The method index here is always with respect
// to the underlying go/types data structures,
// which ultimately derives from source order
// and must be preserved by export data.
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
path2 := appendOpArg(path, opMethod, i)
if m == obj {
return Path(path2), nil // found declared method
}
if r := find(obj, m.Type(), append(path2, opType)); r != nil {
return Path(r), nil
}
}
// Second and later searches: consult the index.
if offset, ok := ix.offsets[obj]; ok {
return ix.path(offset), nil
}
}
return "", fmt.Errorf("can't find path for %v in %s", obj, pkg.Path())
}
func appendOpArg(path []byte, op byte, arg int) []byte {
// traverse performs a complete traversal of all symbols reachable from the package.
func (tr *traversal) traverse() {
scope := tr.pkg.Scope()
names := scope.Names()
if tr.ix != nil {
tr.ix.scopeNames = names
}
empty := make([]byte, 0, 48) // initial space for stack (ix == nil)
// First inspect package-level type names.
// In the presence of path aliases, these give
// the best paths because non-types may
// refer to types, but not the reverse.
for i, name := range names {
if tr.found != "" {
return // found (ix == nil)
}
obj := scope.Lookup(name)
if _, ok := obj.(*types.TypeName); !ok {
continue // handle non-types in second pass
}
// emit (name, opType)
var path []byte
var offset uint32
if tr.ix == nil {
path = append(empty, name...)
path = append(path, opType)
} else {
offset = tr.ix.emitPackageLevel(i)
tr.ix.offsets[obj] = offset
offset = tr.ix.emitPathSegment(offset, opType, -1)
}
// A TypeName (for Named or Alias) may have type parameters.
switch t := obj.Type().(type) {
case *types.Alias:
tr.tparams(t.TypeParams(), path, offset, opTypeParam)
tr.typ(path, offset, opRhs, -1, t.Rhs())
case *types.Named:
tr.tparams(t.TypeParams(), path, offset, opTypeParam)
tr.typ(path, offset, opUnderlying, -1, t.Underlying())
}
}
// Then inspect everything else:
// exported non-types, and declared methods of defined types.
for i, name := range names {
if tr.found != "" {
return // found (ix == nil)
}
obj := scope.Lookup(name)
if tname, ok := obj.(*types.TypeName); !ok {
if obj.Exported() {
// exported non-type (const, var, func)
var path []byte
var offset uint32
if tr.ix == nil {
path = append(empty, name...)
} else {
offset = tr.ix.emitPackageLevel(i)
tr.ix.offsets[obj] = offset
}
tr.typ(path, offset, opType, -1, obj.Type())
}
} else if T, ok := types.Unalias(tname.Type()).(*types.Named); ok {
// defined type
var path []byte
var offset uint32
if tr.ix == nil {
path = append(empty, name...)
path = append(path, opType)
} else {
// Inv: map entry for obj was populated in first pass.
offset = tr.ix.emitPathSegment(tr.ix.offsets[obj], opType, -1)
}
// Inspect declared methods of defined types.
//
// The method index here is always with respect
// to the underlying go/types data structures,
// which ultimately derives from source order
// and must be preserved by export data.
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
tr.object(path, offset, opMethod, i, m)
}
}
}
}
func (tr *traversal) visitType(path []byte, offset uint32, T types.Type) {
switch T := T.(type) {
case *types.Alias:
tr.typ(path, offset, opRhs, -1, T.Rhs())
case *types.Basic, *types.Named:
// Named types belonging to pkg were handled already,
// so T must belong to another package. No path.
return
case *types.Pointer, *types.Slice, *types.Array, *types.Chan:
type hasElem interface{ Elem() types.Type } // note: includes Map
tr.typ(path, offset, opElem, -1, T.(hasElem).Elem())
case *types.Map:
tr.typ(path, offset, opKey, -1, T.Key())
tr.typ(path, offset, opElem, -1, T.Elem())
case *types.Signature:
tr.tparams(T.RecvTypeParams(), path, offset, opRecvTypeParam)
tr.tparams(T.TypeParams(), path, offset, opTypeParam)
tr.typ(path, offset, opParams, -1, T.Params())
tr.typ(path, offset, opResults, -1, T.Results())
case *types.Struct:
for i := 0; i < T.NumFields(); i++ {
tr.object(path, offset, opField, i, T.Field(i))
}
case *types.Tuple:
for i := 0; i < T.Len(); i++ {
tr.object(path, offset, opAt, i, T.At(i))
}
case *types.Interface:
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
if m.Pkg() != nil && m.Pkg() != tr.pkg {
continue // embedded method from another package
}
if !tr.seenMethods[m] {
if tr.seenMethods == nil {
tr.seenMethods = make(map[*types.Func]bool)
}
tr.seenMethods[m] = true
tr.object(path, offset, opMethod, i, m)
}
}
case *types.TypeParam:
tname := T.Obj()
if tname.Pkg() != nil && tname.Pkg() != tr.pkg {
return // type parameter from another package
}
if !tr.seenTParamNames[tname] {
if tr.seenTParamNames == nil {
tr.seenTParamNames = make(map[*types.TypeName]bool)
}
tr.seenTParamNames[tname] = true
tr.object(path, offset, opObj, -1, tname)
tr.typ(path, offset, opConstraint, -1, T.Constraint())
}
}
}
func (tr *traversal) tparams(list *types.TypeParamList, path []byte, offset uint32, op byte) {
for i := 0; i < list.Len(); i++ {
tr.typ(path, offset, op, i, list.At(i))
}
}
// typ descends the type graph edge (op, index), then proceeds to traverse type t.
func (tr *traversal) typ(path []byte, offset uint32, op byte, index int, t types.Type) {
if tr.ix == nil {
path = appendOpArg(path, op, index)
} else {
offset = tr.ix.emitPathSegment(offset, op, index)
}
tr.visitType(path, offset, t)
}
// object descends the type graph edge (op, index), records object
// obj, then proceeds to traverse its type.
func (tr *traversal) object(path []byte, offset uint32, op byte, index int, obj types.Object) {
if tr.ix == nil {
path = appendOpArg(path, op, index)
if obj == tr.target && tr.found == "" {
tr.found = Path(path)
}
path = append(path, opType)
} else {
offset = tr.ix.emitPathSegment(offset, op, index)
if _, ok := tr.ix.offsets[obj]; !ok {
tr.ix.offsets[obj] = offset
}
offset = tr.ix.emitPathSegment(offset, opType, -1)
}
tr.visitType(path, offset, obj.Type())
}
// emitPackageLevel encodes a record for a package-level symbol,
// identified by its index in ix.scopeNames.
func (p *pkgIndex) emitPackageLevel(index int) uint32 {
off := uint32(len(p.data))
p.data = append(p.data, 0) // zero varint => no parent
p.data = binary.AppendUvarint(p.data, uint64(index))
return off
}
// emitPathSegment emits a record for a non-initial object path segment.
func (p *pkgIndex) emitPathSegment(parent uint32, op byte, index int) uint32 {
off := uint32(len(p.data))
p.data = binary.AppendUvarint(p.data, uint64(parent))
p.data = append(p.data, op)
switch op {
case opAt, opField, opMethod, opTypeParam, opRecvTypeParam:
p.data = binary.AppendUvarint(p.data, uint64(index))
}
return off
}
// path returns the Path for the encoded node at the specified offset.
func (p *pkgIndex) path(offset uint32) Path {
var elems []string // path elements in reverse
for {
// Read parent index.
parent, n := binary.Uvarint(p.data[offset:])
offset += uint32(n)
if parent == 0 {
break // root (end of path)
}
op := p.data[offset]
offset++
// The [AFMTr] operators have a numeric operand.
switch op {
case opAt, opField, opMethod, opTypeParam, opRecvTypeParam:
val, n := binary.Uvarint(p.data[offset:])
offset += uint32(n)
elems = append(elems, strconv.Itoa(int(val)))
}
elems = append(elems, string([]byte{op}))
offset = uint32(parent)
}
idx, _ := binary.Uvarint(p.data[offset:])
// Convert index to Path string.
name := p.scopeNames[idx]
sz := len(name)
for _, elem := range elems {
sz += len(elem)
}
var buf strings.Builder
buf.Grow(sz)
buf.WriteString(name)
for _, elem := range slices.Backward(elems) {
buf.WriteString(elem)
}
return Path(buf.String())
}
// appendOpArg appends (op, index) to the object path.
// A negative index is ignored.
func appendOpArg(path []byte, op byte, index int) []byte {
path = append(path, op)
path = strconv.AppendInt(path, int64(arg), 10)
if index >= 0 {
path = strconv.AppendInt(path, int64(index), 10)
}
return path
}
@ -442,138 +723,6 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) {
// panic(fmt.Sprintf("couldn't find method %s on type %s; methods: %#v", meth, named, enc.namedMethods(named)))
}
// find finds obj within type T, returning the path to it, or nil if not found.
//
// The seen map is used to short circuit cycles through type parameters. If
// nil, it will be allocated as necessary.
//
// The seenMethods map is used internally to short circuit cycles through
// interface methods, such as occur in the following example:
//
// type I interface { f() interface{I} }
//
// See golang/go#68046 for details.
func find(obj types.Object, T types.Type, path []byte) []byte {
return (&finder{obj: obj}).find(T, path)
}
// finder closes over search state for a call to find.
type finder struct {
obj types.Object // the sought object
seenTParamNames map[*types.TypeName]bool // for cycle breaking through type parameters
seenMethods map[*types.Func]bool // for cycle breaking through recursive interfaces
}
func (f *finder) find(T types.Type, path []byte) []byte {
switch T := T.(type) {
case *types.Alias:
return f.find(types.Unalias(T), path)
case *types.Basic, *types.Named:
// Named types belonging to pkg were handled already,
// so T must belong to another package. No path.
return nil
case *types.Pointer:
return f.find(T.Elem(), append(path, opElem))
case *types.Slice:
return f.find(T.Elem(), append(path, opElem))
case *types.Array:
return f.find(T.Elem(), append(path, opElem))
case *types.Chan:
return f.find(T.Elem(), append(path, opElem))
case *types.Map:
if r := f.find(T.Key(), append(path, opKey)); r != nil {
return r
}
return f.find(T.Elem(), append(path, opElem))
case *types.Signature:
if r := f.findTypeParam(T.RecvTypeParams(), path, opRecvTypeParam); r != nil {
return r
}
if r := f.findTypeParam(T.TypeParams(), path, opTypeParam); r != nil {
return r
}
if r := f.find(T.Params(), append(path, opParams)); r != nil {
return r
}
return f.find(T.Results(), append(path, opResults))
case *types.Struct:
for i := 0; i < T.NumFields(); i++ {
fld := T.Field(i)
path2 := appendOpArg(path, opField, i)
if fld == f.obj {
return path2 // found field var
}
if r := f.find(fld.Type(), append(path2, opType)); r != nil {
return r
}
}
return nil
case *types.Tuple:
for i := 0; i < T.Len(); i++ {
v := T.At(i)
path2 := appendOpArg(path, opAt, i)
if v == f.obj {
return path2 // found param/result var
}
if r := f.find(v.Type(), append(path2, opType)); r != nil {
return r
}
}
return nil
case *types.Interface:
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
if f.seenMethods[m] {
continue // break cycles (see TestIssue70418)
}
path2 := appendOpArg(path, opMethod, i)
if m == f.obj {
return path2 // found interface method
}
if f.seenMethods == nil {
f.seenMethods = make(map[*types.Func]bool)
}
f.seenMethods[m] = true
if r := f.find(m.Type(), append(path2, opType)); r != nil {
return r
}
}
return nil
case *types.TypeParam:
name := T.Obj()
if f.seenTParamNames[name] {
return nil
}
if name == f.obj {
return append(path, opObj)
}
if f.seenTParamNames == nil {
f.seenTParamNames = make(map[*types.TypeName]bool)
}
f.seenTParamNames[name] = true
if r := f.find(T.Constraint(), append(path, opConstraint)); r != nil {
return r
}
return nil
}
panic(T)
}
func findTypeParam(obj types.Object, list *types.TypeParamList, path []byte, op byte) []byte {
return (&finder{obj: obj}).findTypeParam(list, path, op)
}
func (f *finder) findTypeParam(list *types.TypeParamList, path []byte, op byte) []byte {
for i := 0; i < list.Len(); i++ {
tparam := list.At(i)
path2 := appendOpArg(path, op, i)
if r := f.find(tparam, path2); r != nil {
return r
}
}
return nil
}
// Object returns the object denoted by path p within the package pkg.
func Object(pkg *types.Package, p Path) (types.Object, error) {
pathstr := string(p)
@ -708,7 +857,7 @@ func Object(pkg *types.Package, p Path) (types.Object, error) {
}
tparams := hasTypeParams.TypeParams()
if n := tparams.Len(); index >= n {
return nil, fmt.Errorf("tuple index %d out of range [0-%d)", index, n)
return nil, fmt.Errorf("type parameter index %d out of range [0-%d)", index, n)
}
t = tparams.At(index)
@ -719,7 +868,7 @@ func Object(pkg *types.Package, p Path) (types.Object, error) {
}
rtparams := sig.RecvTypeParams()
if n := rtparams.Len(); index >= n {
return nil, fmt.Errorf("tuple index %d out of range [0-%d)", index, n)
return nil, fmt.Errorf("receiver type parameter index %d out of range [0-%d)", index, n)
}
t = rtparams.At(index)
@ -794,23 +943,3 @@ func Object(pkg *types.Package, p Path) (types.Object, error) {
return obj, nil // success
}
// scopeObjects is a memoization of scope objects.
// Callers must not modify the result.
func (enc *Encoder) scopeObjects(scope *types.Scope) []types.Object {
m := enc.scopeMemo
if m == nil {
m = make(map[*types.Scope][]types.Object)
enc.scopeMemo = m
}
objs, ok := m[scope]
if !ok {
names := scope.Names() // allocates and sorts
objs = make([]types.Object, len(names))
for i, name := range names {
objs[i] = scope.Lookup(name)
}
m[scope] = objs
}
return objs
}

View file

@ -8,6 +8,7 @@ import (
"go/ast"
"go/token"
"iter"
"sort"
"strings"
)
@ -114,18 +115,25 @@ func Directives(g *ast.CommentGroup) (res []*Directive) {
}
// Comments returns an iterator over the comments overlapping the specified interval.
// Comments are sorted by position in the file, so we can use binary search.
func Comments(file *ast.File, start, end token.Pos) iter.Seq[*ast.Comment] {
// TODO(adonovan): optimize use binary O(log n) instead of linear O(n) search.
return func(yield func(*ast.Comment) bool) {
for _, cg := range file.Comments {
for _, co := range cg.List {
// Find the first comment group that overlaps the range.
i := sort.Search(len(file.Comments), func(i int) bool {
return file.Comments[i].End() >= start
})
for _, cg := range file.Comments[i:] {
if cg.Pos() > end {
return
}
// Find the first comment in the group that overlaps the range.
j := sort.Search(len(cg.List), func(j int) bool {
return cg.List[j].End() >= start
})
for _, co := range cg.List[j:] {
if co.Pos() > end {
return
}
if co.End() < start {
continue
}
if !yield(co) {
return
}

View file

@ -0,0 +1,38 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package astutil
import (
"go/ast"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
)
// UnparenCursor returns the cursor for an expression with any
// enclosing parentheses removed, similar to [ast.Unparen].
// It is often prudent to call this before switching on the
// type of cur.Node().
//
// See also [UnparenEnclosingCursor].
func UnparenCursor(cur inspector.Cursor) inspector.Cursor {
for is[*ast.ParenExpr](cur) {
cur, _ = cur.FirstChild()
}
return cur
}
// UnparenEnclosingCursor returns the first element of
// the [Cursor.Enclosing] sequence that is not itself enclosed
// in parens. It is often prudent to call this before switching on
// cur.ParentEdge().
//
// See also [UnparenCursor].
func UnparenEnclosingCursor(cur inspector.Cursor) inspector.Cursor {
for cur.ParentEdgeKind() == edge.ParenExpr_X {
cur = cur.Parent()
}
return cur
}

View file

@ -12,9 +12,14 @@ import (
)
// PurgeFuncBodies returns a copy of src in which the contents of each
// outermost {...} region except struct and interface types have been
// deleted. This reduces the amount of work required to parse the
// top-level declarations.
// outermost {...} region have been deleted, except for struct and
// interface type bodies and the bodies of length-elided array
// literals ([...]T), whose element count is part of the type. It
// includes function bodies, function-literal bodies, and the bodies
// of slice, map, and explicitly-sized array composite literals (whose
// contents don't affect the type of the enclosing declaration). This
// reduces the amount of work required to parse the top-level
// declarations.
//
// PurgeFuncBodies does not preserve newlines or position information.
// Also, if the input is invalid, parsing the output of
@ -22,11 +27,12 @@ import (
// on parser error recovery.
func PurgeFuncBodies(src []byte) []byte {
// Destroy the content of any {...}-bracketed regions that are
// not immediately preceded by a "struct" or "interface"
// token. That includes function bodies, composite literals,
// switch/select bodies, and all blocks of statements.
// This will lead to non-void functions that don't have return
// statements, which of course is a type error, but that's ok.
// not immediately preceded by a "struct" or "interface" token,
// and that are not the body of a length-elided array literal.
// That includes function bodies, switch/select bodies, and most
// composite literals; this will lead to non-void functions that
// don't have return statements, which of course is a type error,
// but that's ok.
var out bytes.Buffer
file := token.NewFileSet().AddFile("", -1, len(src))
@ -34,7 +40,8 @@ func PurgeFuncBodies(src []byte) []byte {
sc.Init(file, src, nil, 0)
var prev token.Token
var cursor int // last consumed src offset
var braces []token.Pos // stack of unclosed braces or -1 for struct/interface type
var braces []token.Pos // stack of unclosed braces, or -1 for a region we preserve
var ellipsis bool // saw "[...]" not yet consumed by a literal-body "{"
for {
pos, tok, _ := sc.Scan()
if tok == token.EOF {
@ -44,9 +51,23 @@ func PurgeFuncBodies(src []byte) []byte {
case token.COMMENT:
// TODO(adonovan): opt: skip, to save an estimated 20% of time.
case token.SEMICOLON:
ellipsis = false
case token.RBRACK:
// "...]" occurs only in the array-type prefix of a
// composite literal; variadic "..." is followed by
// a type or ")", never "]".
if prev == token.ELLIPSIS {
ellipsis = true
}
case token.LBRACE:
if prev == token.STRUCT || prev == token.INTERFACE {
pos = -1
pos = -1 // type body: preserve (don't consume ellipsis)
} else if ellipsis {
pos = -1 // [...]T literal body: preserve
ellipsis = false
}
braces = append(braces, pos)
@ -55,7 +76,7 @@ func PurgeFuncBodies(src []byte) []byte {
top := braces[last]
braces = braces[:last]
if top < 0 {
// struct/interface type: leave alone
// preserve
} else if len(braces) == 0 { // toplevel only
// Delete {...} body.
start := file.Offset(top)

View file

@ -254,3 +254,8 @@ func needsParens(parentNode ast.Node, old, new ast.Expr) bool {
}
return false
}
func is[T any](n any) bool {
_, ok := n.(T)
return ok
}

View file

@ -9,11 +9,12 @@ package goplsexport
import "golang.org/x/tools/go/analysis"
var (
ErrorsAsTypeModernizer *analysis.Analyzer // = modernize.errorsastypeAnalyzer
ErrorsAsTypeModernizer *analysis.Analyzer // = modernize.errorsastypeAnalyzer
SlicesBackwardModernizer *analysis.Analyzer // = modernize.slicesbackwardAnalyzer
StdIteratorsModernizer *analysis.Analyzer // = modernize.stditeratorsAnalyzer
PlusBuildModernizer *analysis.Analyzer // = modernize.plusbuildAnalyzer
StringsCutModernizer *analysis.Analyzer // = modernize.stringscutAnalyzer
UnsafeFuncsModernizer *analysis.Analyzer // = modernize.unsafeFuncsAnalyzer
AtomicTypesModernizer *analysis.Analyzer // = modernize.atomicTypesAnalyzer
StdIteratorsModernizer *analysis.Analyzer // = modernize.stditeratorsAnalyzer
PlusBuildModernizer *analysis.Analyzer // = modernize.plusbuildAnalyzer
StringsCutModernizer *analysis.Analyzer // = modernize.stringscutAnalyzer
UnsafeFuncsModernizer *analysis.Analyzer // = modernize.unsafeFuncsAnalyzer
AtomicTypesModernizer *analysis.Analyzer // = modernize.atomicTypesAnalyzer
EmbedLitModernizer *analysis.Analyzer // = modernize.embedLitAnalyzer
)

View file

@ -53,3 +53,11 @@ func Len[T any](seq iter.Seq[T]) (n int) {
}
return
}
// Empty reports whether the sequence contains no elements.
func Empty[T any](seq iter.Seq[T]) bool {
for range seq {
return false
}
return true
}

View file

@ -439,7 +439,7 @@ Big:
inStmtList = true
case *ast.ForStmt:
use(parent.For, parent.Body.Lbrace)
// special handling, as init;cond;post BlockStmt is not a statment list
// special handling, as init;cond;post BlockStmt is not a statement list
if parent.Init != nil && parent.Cond != nil && stmt == parent.Init && lineOf(parent.Cond.Pos()) == lineOf(stmt.End()) {
rightStmt = parent.Cond.Pos()
} else if parent.Post != nil && parent.Cond != nil && stmt == parent.Post && lineOf(parent.Cond.End()) == lineOf(stmt.Pos()) {

View file

@ -530,9 +530,7 @@ func analyzeTypeParams(_ logger, fset *token.FileSet, info *types.Info, decl *as
// We don't care about most of the properties that matter for parameter references:
// a type is immutable, cannot have its address taken, and does not undergo conversions.
// TODO(jba): can we nevertheless combine this with the traversal in analyzeParams?
var stack []ast.Node
stack = append(stack, decl.Type) // for scope of function itself
ast.PreorderStack(decl.Body, stack, func(n ast.Node, stack []ast.Node) bool {
visit := func(n ast.Node, stack []ast.Node) bool {
if id, ok := n.(*ast.Ident); ok {
if v, ok := info.Uses[id].(*types.TypeName); ok {
if pinfo, ok := paramInfos[v]; ok {
@ -543,7 +541,16 @@ func analyzeTypeParams(_ logger, fset *token.FileSet, info *types.Info, decl *as
}
}
return true
})
}
var stack []ast.Node
stack = append(stack, decl.Type) // for scope of function itself
if decl.Type.Params != nil {
ast.PreorderStack(decl.Type.Params, stack, visit)
}
if decl.Type.Results != nil {
ast.PreorderStack(decl.Type.Results, stack, visit)
}
ast.PreorderStack(decl.Body, stack, visit)
return params
}

View file

@ -2050,7 +2050,7 @@ func resolveEffects(logf logger, args []*argument, effects []int, sg substGraph)
}
}
if !sg.has(argi) {
for j := 0; j < i; j++ {
for j := range i {
argj := args[j]
if argj.pure {
continue

View file

@ -17,6 +17,7 @@ import (
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal"
)
@ -217,10 +218,9 @@ func (ix *Index) Selection(path, typename, name string) types.Object {
func (ix *Index) Calls(callee types.Object) iter.Seq[inspector.Cursor] {
return func(yield func(inspector.Cursor) bool) {
for cur := range ix.Uses(callee) {
ek := cur.ParentEdgeKind()
// The call may be of the form f() or x.f(),
// optionally with parens; ascend from f to call.
// See logic in [typesinternal.UsedIdent], to which this is dual.
//
// It is tempting but wrong to use the first
// CallExpr ancestor: we have to make sure the
@ -229,25 +229,20 @@ func (ix *Index) Calls(callee types.Object) iter.Seq[inspector.Cursor] {
// Avoiding Enclosing is also significantly faster.
// inverse unparen: f -> (f)
for ek == edge.ParenExpr_X {
cur = cur.Parent()
ek = cur.ParentEdgeKind()
cur = astutil.UnparenEnclosingCursor(cur)
// ascend selector (or qualified identifier): f -> x.f
if cur.ParentEdgeKind() == edge.SelectorExpr_Sel {
cur = astutil.UnparenEnclosingCursor(cur.Parent())
}
// ascend selector: f -> x.f
if ek == edge.SelectorExpr_Sel {
cur = cur.Parent()
ek = cur.ParentEdgeKind()
}
// inverse unparen again
for ek == edge.ParenExpr_X {
cur = cur.Parent()
ek = cur.ParentEdgeKind()
// ascend typeparams: f -> f[T]; f -> f[T1, T2]
if ek := cur.ParentEdgeKind(); ek == edge.IndexExpr_X || ek == edge.IndexListExpr_X {
cur = astutil.UnparenEnclosingCursor(cur.Parent())
}
// ascend from f or x.f to call
if ek == edge.CallExpr_Fun {
if cur.ParentEdgeKind() == edge.CallExpr_Fun {
curCall := cur.Parent()
call := curCall.Node().(*ast.CallExpr)
if typeutil.Callee(ix.info, call) == callee {

View file

@ -48,7 +48,7 @@ golang.org/x/sync/semaphore
golang.org/x/sys/plan9
golang.org/x/sys/unix
golang.org/x/sys/windows
# golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa
# golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6
## explicit; go 1.25.0
golang.org/x/telemetry
golang.org/x/telemetry/counter
@ -73,7 +73,7 @@ golang.org/x/text/internal/tag
golang.org/x/text/language
golang.org/x/text/transform
golang.org/x/text/unicode/norm
# golang.org/x/tools v0.44.1-0.20260414062052-55fb96ff894f
# golang.org/x/tools v0.45.1-0.20260520205638-b38156a7a9f5
## explicit; go 1.25.0
golang.org/x/tools/cmd/bisect
golang.org/x/tools/cover