Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringsseq.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/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/typesinternal/typeindex"
    20  	"golang.org/x/tools/internal/versions"
    21  )
    22  
    23  var StringsSeqAnalyzer = &analysis.Analyzer{
    24  	Name: "stringsseq",
    25  	Doc:  analyzerutil.MustExtractDoc(doc, "stringsseq"),
    26  	Requires: []*analysis.Analyzer{
    27  		inspect.Analyzer,
    28  		typeindexanalyzer.Analyzer,
    29  	},
    30  	Run: stringsseq,
    31  	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq",
    32  }
    33  
    34  // stringsseq offers a fix to replace a call to strings.Split with
    35  // SplitSeq or strings.Fields with FieldsSeq
    36  // when it is the operand of a range loop, either directly:
    37  //
    38  //	for _, line := range strings.Split() {...}
    39  //
    40  // or indirectly, if the variable's sole use is the range statement:
    41  //
    42  //	lines := strings.Split()
    43  //	for _, line := range lines {...}
    44  //
    45  // Variants:
    46  // - bytes.SplitSeq
    47  // - bytes.FieldsSeq
    48  func stringsseq(pass *analysis.Pass) (any, error) {
    49  	var (
    50  		index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
    51  		info  = pass.TypesInfo
    52  
    53  		stringsSplit  = index.Object("strings", "Split")
    54  		stringsFields = index.Object("strings", "Fields")
    55  		bytesSplit    = index.Object("bytes", "Split")
    56  		bytesFields   = index.Object("bytes", "Fields")
    57  	)
    58  	if !index.Used(stringsSplit, stringsFields, bytesSplit, bytesFields) {
    59  		return nil, nil
    60  	}
    61  
    62  	for curFile := range filesUsingGoVersion(pass, versions.Go1_24) {
    63  		for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
    64  			rng := curRange.Node().(*ast.RangeStmt)
    65  
    66  			// Reject "for i, line := ..." since SplitSeq is not an iter.Seq2.
    67  			// (We require that i is blank.)
    68  			if id, ok := rng.Key.(*ast.Ident); ok && id.Name != "_" {
    69  				continue
    70  			}
    71  
    72  			// Find the call operand of the range statement,
    73  			// whether direct or indirect.
    74  			call, ok := rng.X.(*ast.CallExpr)
    75  			if !ok {
    76  				if id, ok := rng.X.(*ast.Ident); ok {
    77  					if v, ok := info.Uses[id].(*types.Var); ok {
    78  						if ek, idx := curRange.ParentEdge(); ek == edge.BlockStmt_List && idx > 0 {
    79  							curPrev, _ := curRange.PrevSibling()
    80  							if assign, ok := curPrev.Node().(*ast.AssignStmt); ok &&
    81  								assign.Tok == token.DEFINE &&
    82  								len(assign.Lhs) == 1 &&
    83  								len(assign.Rhs) == 1 &&
    84  								info.Defs[assign.Lhs[0].(*ast.Ident)] == v &&
    85  								soleUseIs(index, v, id) {
    86  								// Have:
    87  								//    lines := ...
    88  								//    for _, line := range lines {...}
    89  								// and no other uses of lines.
    90  								call, _ = assign.Rhs[0].(*ast.CallExpr)
    91  							}
    92  						}
    93  					}
    94  				}
    95  			}
    96  
    97  			if call != nil {
    98  				var edits []analysis.TextEdit
    99  				if rng.Key != nil {
   100  					// Delete (blank) RangeStmt.Key:
   101  					//  for _, line := -> for line :=
   102  					//  for _, _    := -> for
   103  					//  for _       := -> for
   104  					end := rng.Range
   105  					if rng.Value != nil {
   106  						end = rng.Value.Pos()
   107  					}
   108  					edits = append(edits, analysis.TextEdit{
   109  						Pos: rng.Key.Pos(),
   110  						End: end,
   111  					})
   112  				}
   113  
   114  				sel, ok := call.Fun.(*ast.SelectorExpr)
   115  				if !ok {
   116  					continue
   117  				}
   118  
   119  				switch obj := typeutil.Callee(info, call); obj {
   120  				case stringsSplit, stringsFields, bytesSplit, bytesFields:
   121  					oldFnName := obj.Name()
   122  					seqFnName := fmt.Sprintf("%sSeq", oldFnName)
   123  					pass.Report(analysis.Diagnostic{
   124  						Pos:     sel.Pos(),
   125  						End:     sel.End(),
   126  						Message: fmt.Sprintf("Ranging over %s is more efficient", seqFnName),
   127  						SuggestedFixes: []analysis.SuggestedFix{{
   128  							Message: fmt.Sprintf("Replace %s with %s", oldFnName, seqFnName),
   129  							TextEdits: append(edits, analysis.TextEdit{
   130  								Pos:     sel.Sel.Pos(),
   131  								End:     sel.Sel.End(),
   132  								NewText: []byte(seqFnName)}),
   133  						}},
   134  					})
   135  				}
   136  			}
   137  		}
   138  	}
   139  	return nil, nil
   140  }
   141  

View as plain text