Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/modernize/rangeint.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package modernize
     6  
     7  import (
     8  	"fmt"
     9  	"go/ast"
    10  	"go/token"
    11  	"go/types"
    12  
    13  	"golang.org/x/tools/go/analysis"
    14  	"golang.org/x/tools/go/analysis/passes/inspect"
    15  	"golang.org/x/tools/go/ast/edge"
    16  	"golang.org/x/tools/go/ast/inspector"
    17  	"golang.org/x/tools/go/types/typeutil"
    18  	"golang.org/x/tools/internal/analysis/analyzerutil"
    19  	typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
    20  	"golang.org/x/tools/internal/astutil"
    21  	"golang.org/x/tools/internal/typesinternal"
    22  	"golang.org/x/tools/internal/typesinternal/typeindex"
    23  	"golang.org/x/tools/internal/versions"
    24  )
    25  
    26  var RangeIntAnalyzer = &analysis.Analyzer{
    27  	Name: "rangeint",
    28  	Doc:  analyzerutil.MustExtractDoc(doc, "rangeint"),
    29  	Requires: []*analysis.Analyzer{
    30  		inspect.Analyzer,
    31  		typeindexanalyzer.Analyzer,
    32  	},
    33  	Run: rangeint,
    34  	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint",
    35  }
    36  
    37  // rangeint offers a fix to replace a 3-clause 'for' loop:
    38  //
    39  //	for i := 0; i < limit; i++ {}
    40  //
    41  // by a range loop with an integer operand:
    42  //
    43  //	for i := range limit {}
    44  //
    45  // Variants:
    46  //   - The ':=' may be replaced by '='.
    47  //   - The fix may remove "i :=" if it would become unused.
    48  //
    49  // Restrictions:
    50  //   - The variable i must not be assigned or address-taken within the
    51  //     loop, because a "for range int" loop does not respect assignments
    52  //     to the loop index.
    53  //   - The limit must not be b.N, to avoid redundancy with bloop's fixes.
    54  //
    55  // Caveats:
    56  //
    57  // The fix causes the limit expression to be evaluated exactly once,
    58  // instead of once per iteration. So, to avoid changing the
    59  // cardinality of side effects, the limit expression must not involve
    60  // function calls (e.g. seq.Len()) or channel receives. Moreover, the
    61  // value of the limit expression must be loop invariant, which in
    62  // practice means it must take one of the following forms:
    63  //
    64  //   - a local variable that is assigned only once and not address-taken;
    65  //   - a constant; or
    66  //   - len(s), where s has the above properties.
    67  func rangeint(pass *analysis.Pass) (any, error) {
    68  	var (
    69  		info      = pass.TypesInfo
    70  		typeindex = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
    71  	)
    72  
    73  	for curFile := range filesUsingGoVersion(pass, versions.Go1_22) {
    74  	nextLoop:
    75  		for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) {
    76  			loop := curLoop.Node().(*ast.ForStmt)
    77  			if init, ok := loop.Init.(*ast.AssignStmt); ok &&
    78  				isSimpleAssign(init) &&
    79  				is[*ast.Ident](init.Lhs[0]) &&
    80  				isZeroIntConst(info, init.Rhs[0]) {
    81  				// Have: for i = 0; ... (or i := 0)
    82  				index := init.Lhs[0].(*ast.Ident)
    83  
    84  				if compare, ok := loop.Cond.(*ast.BinaryExpr); ok &&
    85  					compare.Op == token.LSS &&
    86  					astutil.EqualSyntax(compare.X, init.Lhs[0]) {
    87  					// Have: for i = 0; i < limit; ... {}
    88  
    89  					limit := compare.Y
    90  
    91  					// If limit is "len(slice)", simplify it to "slice".
    92  					//
    93  					// (Don't replace "for i := 0; i < len(map); i++"
    94  					// with "for range m" because it's too hard to prove
    95  					// that len(m) is loop-invariant).
    96  					if call, ok := limit.(*ast.CallExpr); ok &&
    97  						typeutil.Callee(info, call) == builtinLen &&
    98  						is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) {
    99  						limit = call.Args[0]
   100  					}
   101  
   102  					// Check the form of limit: must be a constant,
   103  					// or a local var that is not assigned or address-taken.
   104  					limitOK := false
   105  					if info.Types[limit].Value != nil {
   106  						limitOK = true // constant
   107  					} else if id, ok := limit.(*ast.Ident); ok {
   108  						if v, ok := info.Uses[id].(*types.Var); ok &&
   109  							!(v.Exported() && typesinternal.IsPackageLevel(v)) {
   110  							// limit is a local or unexported global var.
   111  							// (An exported global may have uses we can't see.)
   112  							for cur := range typeindex.Uses(v) {
   113  								if isScalarLvalue(info, cur) {
   114  									// Limit var is assigned or address-taken.
   115  									continue nextLoop
   116  								}
   117  							}
   118  							limitOK = true
   119  						}
   120  					}
   121  					if !limitOK {
   122  						continue nextLoop
   123  					}
   124  
   125  					if inc, ok := loop.Post.(*ast.IncDecStmt); ok &&
   126  						inc.Tok == token.INC &&
   127  						astutil.EqualSyntax(compare.X, inc.X) {
   128  						// Have: for i = 0; i < limit; i++ {}
   129  
   130  						// Find references to i within the loop body.
   131  						v := info.ObjectOf(index).(*types.Var)
   132  						// TODO(adonovan): use go1.25 v.Kind() == types.PackageVar
   133  						if typesinternal.IsPackageLevel(v) {
   134  							continue nextLoop
   135  						}
   136  						used := false
   137  						for curId := range curLoop.Child(loop.Body).Preorder((*ast.Ident)(nil)) {
   138  							id := curId.Node().(*ast.Ident)
   139  							if info.Uses[id] == v {
   140  								used = true
   141  
   142  								// Reject if any is an l-value (assigned or address-taken):
   143  								// a "for range int" loop does not respect assignments to
   144  								// the loop variable.
   145  								if isScalarLvalue(info, curId) {
   146  									continue nextLoop
   147  								}
   148  							}
   149  						}
   150  
   151  						// If i is no longer used, delete "i := ".
   152  						var edits []analysis.TextEdit
   153  						if !used && init.Tok == token.DEFINE {
   154  							edits = append(edits, analysis.TextEdit{
   155  								Pos: index.Pos(),
   156  								End: init.Rhs[0].Pos(),
   157  							})
   158  						}
   159  
   160  						// If i is used after the loop,
   161  						// don't offer a fix, as a range loop
   162  						// leaves i with a different final value (limit-1).
   163  						if init.Tok == token.ASSIGN {
   164  							// Find the nearest ancestor that is not a label.
   165  							// Otherwise, checking for i usage outside of a for
   166  							// loop might not function properly further below.
   167  							// This is because the i usage might be a child of
   168  							// the loop's parent's parent, for example:
   169  							//     var i int
   170  							// Loop:
   171  							//     for i = 0; i < 10; i++ { break loop }
   172  							//     // i is in the sibling of the label, not the loop
   173  							//     fmt.Println(i)
   174  							//
   175  							ancestor := curLoop.Parent()
   176  							for is[*ast.LabeledStmt](ancestor.Node()) {
   177  								ancestor = ancestor.Parent()
   178  							}
   179  							for curId := range ancestor.Preorder((*ast.Ident)(nil)) {
   180  								id := curId.Node().(*ast.Ident)
   181  								if info.Uses[id] == v {
   182  									// Is i used after loop?
   183  									if id.Pos() > loop.End() {
   184  										continue nextLoop
   185  									}
   186  									// Is i used within a defer statement
   187  									// that is within the scope of i?
   188  									//     var i int
   189  									//     defer func() { print(i)}
   190  									//     for i = ... { ... }
   191  									for curDefer := range curId.Enclosing((*ast.DeferStmt)(nil)) {
   192  										if curDefer.Node().Pos() > v.Pos() {
   193  											continue nextLoop
   194  										}
   195  									}
   196  								}
   197  							}
   198  						}
   199  
   200  						// If limit is len(slice),
   201  						// simplify "range len(slice)" to "range slice".
   202  						if call, ok := limit.(*ast.CallExpr); ok &&
   203  							typeutil.Callee(info, call) == builtinLen &&
   204  							is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) {
   205  							limit = call.Args[0]
   206  						}
   207  
   208  						// If the limit is a untyped constant of non-integer type,
   209  						// such as "const limit = 1e3", its effective type may
   210  						// differ between the two forms.
   211  						// In a for loop, it must be comparable with int i,
   212  						//    for i := 0; i < limit; i++
   213  						// but in a range loop it would become a float,
   214  						//    for i := range limit {}
   215  						// which is a type error. We need to convert it to int
   216  						// in this case.
   217  						//
   218  						// Unfortunately go/types discards the untyped type
   219  						// (but see Untyped in golang/go#70638) so we must
   220  						// re-type check the expression to detect this case.
   221  						var beforeLimit, afterLimit string
   222  						if v := info.Types[limit].Value; v != nil {
   223  							tVar := info.TypeOf(init.Rhs[0])
   224  							file := curFile.Node().(*ast.File)
   225  							// TODO(mkalil): use a types.Qualifier that respects the existing
   226  							// imports of this file that are visible (not shadowed) at the current position.
   227  							qual := typesinternal.FileQualifier(file, pass.Pkg)
   228  							beforeLimit, afterLimit = fmt.Sprintf("%s(", types.TypeString(tVar, qual)), ")"
   229  							info2 := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)}
   230  							if types.CheckExpr(pass.Fset, pass.Pkg, limit.Pos(), limit, info2) == nil {
   231  								tLimit := types.Default(info2.TypeOf(limit))
   232  								if types.AssignableTo(tLimit, tVar) {
   233  									beforeLimit, afterLimit = "", ""
   234  								}
   235  							}
   236  						}
   237  
   238  						pass.Report(analysis.Diagnostic{
   239  							Pos:     init.Pos(),
   240  							End:     inc.End(),
   241  							Message: "for loop can be modernized using range over int",
   242  							SuggestedFixes: []analysis.SuggestedFix{{
   243  								Message: fmt.Sprintf("Replace for loop with range %s",
   244  									astutil.Format(pass.Fset, limit)),
   245  								TextEdits: append(edits, []analysis.TextEdit{
   246  									// for i := 0; i < limit; i++ {}
   247  									//     -----              ---
   248  									//          -------
   249  									// for i := range  limit      {}
   250  
   251  									// Delete init.
   252  									{
   253  										Pos:     init.Rhs[0].Pos(),
   254  										End:     limit.Pos(),
   255  										NewText: []byte("range "),
   256  									},
   257  									// Add "int(" before limit, if needed.
   258  									{
   259  										Pos:     limit.Pos(),
   260  										End:     limit.Pos(),
   261  										NewText: []byte(beforeLimit),
   262  									},
   263  									// Delete inc.
   264  									{
   265  										Pos: limit.End(),
   266  										End: inc.End(),
   267  									},
   268  									// Add ")" after limit, if needed.
   269  									{
   270  										Pos:     limit.End(),
   271  										End:     limit.End(),
   272  										NewText: []byte(afterLimit),
   273  									},
   274  								}...),
   275  							}},
   276  						})
   277  					}
   278  				}
   279  			}
   280  		}
   281  	}
   282  	return nil, nil
   283  }
   284  
   285  // isScalarLvalue reports whether the specified identifier is
   286  // address-taken or appears on the left side of an assignment.
   287  //
   288  // This function is valid only for scalars (x = ...),
   289  // not for aggregates (x.a[i] = ...)
   290  func isScalarLvalue(info *types.Info, curId inspector.Cursor) bool {
   291  	// Unfortunately we can't simply use info.Types[e].Assignable()
   292  	// as it is always true for a variable even when that variable is
   293  	// used only as an r-value. So we must inspect enclosing syntax.
   294  
   295  	cur := curId
   296  
   297  	// Strip enclosing parens.
   298  	ek, _ := cur.ParentEdge()
   299  	for ek == edge.ParenExpr_X {
   300  		cur = cur.Parent()
   301  		ek, _ = cur.ParentEdge()
   302  	}
   303  
   304  	switch ek {
   305  	case edge.AssignStmt_Lhs:
   306  		assign := cur.Parent().Node().(*ast.AssignStmt)
   307  		if assign.Tok != token.DEFINE {
   308  			return true // i = j or i += j
   309  		}
   310  		id := curId.Node().(*ast.Ident)
   311  		if v, ok := info.Defs[id]; ok && v.Pos() != id.Pos() {
   312  			return true // reassignment of i (i, j := 1, 2)
   313  		}
   314  	case edge.IncDecStmt_X:
   315  		return true // i++, i--
   316  	case edge.UnaryExpr_X:
   317  		if cur.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
   318  			return true // &i
   319  		}
   320  	}
   321  	return false
   322  }
   323  

View as plain text