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  	"log"
    13  
    14  	"golang.org/x/tools/go/analysis"
    15  	"golang.org/x/tools/go/analysis/passes/inspect"
    16  	"golang.org/x/tools/go/types/typeutil"
    17  	"golang.org/x/tools/internal/analysis/analyzerutil"
    18  	typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
    19  	"golang.org/x/tools/internal/astutil"
    20  	"golang.org/x/tools/internal/typeparams"
    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 typesinternal.IsAssignedOrAddressTaken(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  					validIncrement := false
   126  					if inc, ok := loop.Post.(*ast.IncDecStmt); ok &&
   127  						inc.Tok == token.INC &&
   128  						astutil.EqualSyntax(compare.X, inc.X) {
   129  						// Have: i++
   130  						validIncrement = true
   131  					} else if assign, ok := loop.Post.(*ast.AssignStmt); ok &&
   132  						assign.Tok == token.ADD_ASSIGN &&
   133  						len(assign.Rhs) == 1 && isIntLiteral(info, assign.Rhs[0], 1) &&
   134  						len(assign.Lhs) == 1 && astutil.EqualSyntax(compare.X, assign.Lhs[0]) {
   135  						// Have: i += 1
   136  						validIncrement = true
   137  					}
   138  
   139  					if validIncrement {
   140  						// Have: for i = 0; i < limit; i++ {}
   141  
   142  						// Find references to i within the loop body.
   143  						v := info.ObjectOf(index).(*types.Var)
   144  						switch v.Kind() {
   145  						case types.PackageVar:
   146  							continue nextLoop
   147  						case types.ResultVar:
   148  							// If v is a named result, it is implicitly
   149  							// used after the loop (go.dev/issue/76880).
   150  							continue nextLoop
   151  						}
   152  
   153  						used := false
   154  						for curId := range curLoop.Child(loop.Body).Preorder((*ast.Ident)(nil)) {
   155  							id := curId.Node().(*ast.Ident)
   156  							if info.Uses[id] == v {
   157  								used = true
   158  
   159  								// Reject if any is an l-value (assigned or address-taken):
   160  								// a "for range int" loop does not respect assignments to
   161  								// the loop variable.
   162  								if typesinternal.IsAssignedOrAddressTaken(info, curId) {
   163  									continue nextLoop
   164  								}
   165  							}
   166  						}
   167  
   168  						// If i is no longer used, delete "i := ".
   169  						var edits []analysis.TextEdit
   170  						if !used && init.Tok == token.DEFINE {
   171  							edits = append(edits, analysis.TextEdit{
   172  								Pos: index.Pos(),
   173  								End: init.Rhs[0].Pos(),
   174  							})
   175  						}
   176  
   177  						// If i is used after the loop,
   178  						// don't offer a fix, as a range loop
   179  						// leaves i with a different final value (limit-1).
   180  						if init.Tok == token.ASSIGN {
   181  							// Find the nearest ancestor that is not a label.
   182  							// Otherwise, checking for i usage outside of a for
   183  							// loop might not function properly further below.
   184  							// This is because the i usage might be a child of
   185  							// the loop's parent's parent, for example:
   186  							//     var i int
   187  							// Loop:
   188  							//     for i = 0; i < 10; i++ { break loop }
   189  							//     // i is in the sibling of the label, not the loop
   190  							//     fmt.Println(i)
   191  							//
   192  							ancestor := curLoop.Parent()
   193  							for is[*ast.LabeledStmt](ancestor.Node()) {
   194  								ancestor = ancestor.Parent()
   195  							}
   196  							for curId := range ancestor.Preorder((*ast.Ident)(nil)) {
   197  								id := curId.Node().(*ast.Ident)
   198  								if info.Uses[id] == v {
   199  									// Is i used after loop?
   200  									if id.Pos() > loop.End() {
   201  										continue nextLoop
   202  									}
   203  									// Is i used within a defer statement
   204  									// that is within the scope of i?
   205  									//     var i int
   206  									//     defer func() { print(i)}
   207  									//     for i = ... { ... }
   208  									for curDefer := range curId.Enclosing((*ast.DeferStmt)(nil)) {
   209  										if curDefer.Node().Pos() > v.Pos() {
   210  											continue nextLoop
   211  										}
   212  									}
   213  								}
   214  							}
   215  						}
   216  
   217  						// The loop index (v) must not be a type parameter constrained by
   218  						// multiple distinct integer types, or a type parameter constrained
   219  						// by non-integer types. Transforming such instances to a range loop
   220  						// would result in a compiler error.
   221  						// See golang/go#78571.
   222  						terms, err := typeparams.NormalTerms(v.Type()) // NormalTerms works for any type
   223  						if err != nil {
   224  							log.Fatalf("internal error: cannot compute type set of loop var %v: %v", v, err)
   225  						}
   226  						if len(terms) != 0 {
   227  							// From the spec (https://go.dev/ref/spec#For_range):
   228  							// "If the type of the range expression is a type parameter, all
   229  							// types in its type set must have the same underlying type and the
   230  							// range expression must be valid for that type."
   231  							//
   232  							// Check if all terms have the same underlying type by comparing
   233  							// them to the first term.
   234  							u := terms[0].Type().Underlying()
   235  							// If the constraint has any non-integer terms, skip. (Range over
   236  							// float is not allowed.)
   237  							if !isInteger(u) {
   238  								continue nextLoop
   239  							}
   240  							for _, term := range terms[1:] {
   241  								if !types.Identical(u, term.Type().Underlying()) {
   242  									continue nextLoop
   243  								}
   244  							}
   245  						}
   246  
   247  						// If limit is len(slice),
   248  						// simplify "range len(slice)" to "range slice".
   249  						if call, ok := limit.(*ast.CallExpr); ok &&
   250  							typeutil.Callee(info, call) == builtinLen &&
   251  							is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) {
   252  							limit = call.Args[0]
   253  						}
   254  
   255  						// If the limit is a untyped constant of non-integer type,
   256  						// such as "const limit = 1e3", its effective type may
   257  						// differ between the two forms.
   258  						// In a for loop, it must be comparable with int i,
   259  						//    for i := 0; i < limit; i++ {}
   260  						// but in a range loop it would become a float,
   261  						//    for i := range limit {}
   262  						// which is a type error. We need to convert it to int
   263  						// in this case.
   264  						//
   265  						// Unfortunately go/types discards the untyped type
   266  						// (but see Untyped in golang/go#70638) so we must
   267  						// re-type check the expression to detect this case.
   268  						var beforeLimit, afterLimit string
   269  						if v := info.Types[limit].Value; v != nil {
   270  							tVar := info.TypeOf(init.Rhs[0])
   271  							file := curFile.Node().(*ast.File)
   272  							// TODO(mkalil): use a types.Qualifier that respects the existing
   273  							// imports of this file that are visible (not shadowed) at the current position.
   274  							qual := typesinternal.FileQualifier(file, pass.Pkg)
   275  							beforeLimit, afterLimit = fmt.Sprintf("%s(", types.TypeString(tVar, qual)), ")"
   276  							info2 := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)}
   277  							if types.CheckExpr(pass.Fset, pass.Pkg, limit.Pos(), limit, info2) == nil {
   278  								tLimit := info2.TypeOf(limit)
   279  								// Eliminate conversion when safe.
   280  								//
   281  								// Redundant conversions are not only unsightly but may in some cases cause
   282  								// architecture-specific types (e.g. syscall.Timespec.Nsec) to be inserted
   283  								// into otherwise portable files.
   284  								//
   285  								// The operand must have an integer type (not, say, '1e6')
   286  								// even when assigning to an existing integer variable.
   287  								if isInteger(tLimit) {
   288  									// When declaring a new var from an untyped limit,
   289  									// the limit's default type is what matters.
   290  									if init.Tok != token.ASSIGN {
   291  										tLimit = types.Default(tLimit)
   292  									}
   293  									if types.AssignableTo(tLimit, tVar) {
   294  										beforeLimit, afterLimit = "", ""
   295  									}
   296  								}
   297  							}
   298  						}
   299  
   300  						pass.Report(analysis.Diagnostic{
   301  							Pos:     init.Pos(),
   302  							End:     loop.Post.End(),
   303  							Message: "for loop can be modernized using range over int",
   304  							SuggestedFixes: []analysis.SuggestedFix{{
   305  								Message: fmt.Sprintf("Replace for loop with range %s",
   306  									astutil.Format(pass.Fset, limit)),
   307  								TextEdits: append(edits, []analysis.TextEdit{
   308  									// for i := 0; i < limit; i++ {}
   309  									//     -----              ---
   310  									//          -------
   311  									// for i := range  limit      {}
   312  
   313  									// Delete init.
   314  									{
   315  										Pos:     init.Rhs[0].Pos(),
   316  										End:     limit.Pos(),
   317  										NewText: []byte("range "),
   318  									},
   319  									// Add "int(" before limit, if needed.
   320  									{
   321  										Pos:     limit.Pos(),
   322  										End:     limit.Pos(),
   323  										NewText: []byte(beforeLimit),
   324  									},
   325  									// Delete inc.
   326  									{
   327  										Pos: limit.End(),
   328  										End: loop.Post.End(),
   329  									},
   330  									// Add ")" after limit, if needed.
   331  									{
   332  										Pos:     limit.End(),
   333  										End:     limit.End(),
   334  										NewText: []byte(afterLimit),
   335  									},
   336  								}...),
   337  							}},
   338  						})
   339  					}
   340  				}
   341  			}
   342  		}
   343  	}
   344  	return nil, nil
   345  }
   346  

View as plain text