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

     1  // Copyright 2024 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  	"cmp"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/constant"
    12  	"go/token"
    13  	"go/types"
    14  	"maps"
    15  	"slices"
    16  
    17  	"golang.org/x/tools/go/analysis"
    18  	"golang.org/x/tools/go/analysis/passes/inspect"
    19  	"golang.org/x/tools/go/ast/edge"
    20  	"golang.org/x/tools/go/ast/inspector"
    21  	"golang.org/x/tools/internal/analysis/analyzerutil"
    22  	typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
    23  	"golang.org/x/tools/internal/astutil"
    24  	"golang.org/x/tools/internal/refactor"
    25  	"golang.org/x/tools/internal/typesinternal"
    26  	"golang.org/x/tools/internal/typesinternal/typeindex"
    27  )
    28  
    29  var StringsBuilderAnalyzer = &analysis.Analyzer{
    30  	Name: "stringsbuilder",
    31  	Doc:  analyzerutil.MustExtractDoc(doc, "stringsbuilder"),
    32  	Requires: []*analysis.Analyzer{
    33  		inspect.Analyzer,
    34  		typeindexanalyzer.Analyzer,
    35  	},
    36  	Run: stringsbuilder,
    37  	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringbuilder",
    38  }
    39  
    40  // stringsbuilder replaces string += string in a loop by strings.Builder.
    41  func stringsbuilder(pass *analysis.Pass) (any, error) {
    42  	// Skip the analyzer in packages where its
    43  	// fixes would create an import cycle.
    44  	if within(pass, "strings", "runtime") {
    45  		return nil, nil
    46  	}
    47  
    48  	var (
    49  		inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    50  		index   = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
    51  	)
    52  
    53  	// Gather all local string variables that appear on the
    54  	// LHS of some string += string assignment.
    55  	candidates := make(map[*types.Var]bool)
    56  	for curAssign := range inspect.Root().Preorder((*ast.AssignStmt)(nil)) {
    57  		assign := curAssign.Node().(*ast.AssignStmt)
    58  		if assign.Tok == token.ADD_ASSIGN && is[*ast.Ident](assign.Lhs[0]) {
    59  			if v, ok := pass.TypesInfo.Uses[assign.Lhs[0].(*ast.Ident)].(*types.Var); ok &&
    60  				!typesinternal.IsPackageLevel(v) && // TODO(adonovan): in go1.25, use v.Kind() == types.LocalVar &&
    61  				types.Identical(v.Type(), builtinString.Type()) {
    62  				candidates[v] = true
    63  			}
    64  		}
    65  	}
    66  
    67  	lexicalOrder := func(x, y *types.Var) int { return cmp.Compare(x.Pos(), y.Pos()) }
    68  
    69  	// File and Pos of last fix edit,
    70  	// for overlapping fix span detection.
    71  	var (
    72  		lastEditFile *ast.File
    73  		lastEditEnd  token.Pos
    74  	)
    75  
    76  	// Now check each candidate variable's decl and uses.
    77  nextcand:
    78  	for _, v := range slices.SortedFunc(maps.Keys(candidates), lexicalOrder) {
    79  		var edits []analysis.TextEdit
    80  
    81  		// Check declaration of s:
    82  		//
    83  		//    s := expr
    84  		//    var s [string] [= expr]
    85  		//
    86  		// and transform to:
    87  		//
    88  		//    var s strings.Builder; s.WriteString(expr)
    89  		//
    90  		def, ok := index.Def(v)
    91  		if !ok {
    92  			continue
    93  		}
    94  
    95  		// To avoid semantic conflicts, do not offer a fix if its edit
    96  		// range (ignoring import edits) overlaps a previous fix.
    97  		// This fixes #76983 and is an ad-hoc mitigation of #76476.
    98  		file := astutil.EnclosingFile(def)
    99  		if file == lastEditFile && v.Pos() < lastEditEnd {
   100  			continue
   101  		}
   102  
   103  		ek, _ := def.ParentEdge()
   104  		if ek == edge.AssignStmt_Lhs &&
   105  			len(def.Parent().Node().(*ast.AssignStmt).Lhs) == 1 {
   106  			// Have: s := expr
   107  			// => var s strings.Builder; s.WriteString(expr)
   108  
   109  			assign := def.Parent().Node().(*ast.AssignStmt)
   110  
   111  			// Reject "if s := f(); ..." since in that context
   112  			// we can't replace the assign with two statements.
   113  			switch def.Parent().Parent().Node().(type) {
   114  			case *ast.BlockStmt, *ast.CaseClause, *ast.CommClause:
   115  				// OK: these are the parts of syntax that
   116  				// allow unrestricted statement lists.
   117  			default:
   118  				continue
   119  			}
   120  
   121  			// Add strings import.
   122  			prefix, importEdits := refactor.AddImport(
   123  				pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
   124  			edits = append(edits, importEdits...)
   125  
   126  			if isEmptyString(pass.TypesInfo, assign.Rhs[0]) {
   127  				// s := ""
   128  				// ---------------------
   129  				// var s strings.Builder
   130  				edits = append(edits, analysis.TextEdit{
   131  					Pos:     assign.Pos(),
   132  					End:     assign.End(),
   133  					NewText: fmt.Appendf(nil, "var %[1]s %[2]sBuilder", v.Name(), prefix),
   134  				})
   135  
   136  			} else {
   137  				// s :=                                 expr
   138  				// -------------------------------------    -
   139  				// var s strings.Builder; s.WriteString(expr)
   140  				edits = append(edits, []analysis.TextEdit{
   141  					{
   142  						Pos:     assign.Pos(),
   143  						End:     assign.Rhs[0].Pos(),
   144  						NewText: fmt.Appendf(nil, "var %[1]s %[2]sBuilder; %[1]s.WriteString(", v.Name(), prefix),
   145  					},
   146  					{
   147  						Pos:     assign.End(),
   148  						End:     assign.End(),
   149  						NewText: []byte(")"),
   150  					},
   151  				}...)
   152  
   153  			}
   154  
   155  		} else if ek == edge.ValueSpec_Names &&
   156  			len(def.Parent().Node().(*ast.ValueSpec).Names) == 1 {
   157  			// Have: var s [string] [= expr]
   158  			// => var s strings.Builder; s.WriteString(expr)
   159  
   160  			// Add strings import.
   161  			prefix, importEdits := refactor.AddImport(
   162  				pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
   163  			edits = append(edits, importEdits...)
   164  
   165  			spec := def.Parent().Node().(*ast.ValueSpec)
   166  			decl := def.Parent().Parent().Node().(*ast.GenDecl)
   167  
   168  			init := spec.Names[0].End() // start of " = expr"
   169  			if spec.Type != nil {
   170  				init = spec.Type.End()
   171  			}
   172  
   173  			// var s [string]
   174  			//      ----------------
   175  			// var s strings.Builder
   176  			edits = append(edits, analysis.TextEdit{
   177  				Pos:     spec.Names[0].End(),
   178  				End:     init,
   179  				NewText: fmt.Appendf(nil, " %sBuilder", prefix),
   180  			})
   181  
   182  			if len(spec.Values) > 0 && !isEmptyString(pass.TypesInfo, spec.Values[0]) {
   183  				// =               expr
   184  				// ----------------    -
   185  				// ; s.WriteString(expr)
   186  				edits = append(edits, []analysis.TextEdit{
   187  					{
   188  						Pos:     init,
   189  						End:     spec.Values[0].Pos(),
   190  						NewText: fmt.Appendf(nil, "; %s.WriteString(", v.Name()),
   191  					},
   192  					{
   193  						Pos:     decl.End(),
   194  						End:     decl.End(),
   195  						NewText: []byte(")"),
   196  					},
   197  				}...)
   198  			} else {
   199  				// delete "= expr"
   200  				edits = append(edits, analysis.TextEdit{
   201  					Pos: init,
   202  					End: spec.End(),
   203  				})
   204  			}
   205  
   206  		} else {
   207  			continue
   208  		}
   209  
   210  		// Check uses of s.
   211  		//
   212  		// - All uses of s except the final one must be of the form
   213  		//
   214  		//    s += expr
   215  		//
   216  		//   Each of these will become s.WriteString(expr).
   217  		//   At least one of them must be in an intervening loop
   218  		//   w.r.t. the declaration of s:
   219  		//
   220  		//    var s string
   221  		//    for ... { s += expr }
   222  		//
   223  		// - The final use of s must be as an rvalue (e.g. use(s), not &s).
   224  		//   This will become s.String().
   225  		//
   226  		//   Perhaps surprisingly, it is fine for there to be an
   227  		//   intervening loop or lambda w.r.t. the declaration of s:
   228  		//
   229  		//    var s strings.Builder
   230  		//    for range kSmall { s.WriteString(expr) }
   231  		//    for range kLarge { use(s.String()) } // called repeatedly
   232  		//
   233  		//   Even though that might cause the s.String() operation to be
   234  		//   executed repeatedly, this is not a deoptimization because,
   235  		//   by design, (*strings.Builder).String does not allocate.
   236  		var (
   237  			numLoopAssigns int             // number of += assignments within a loop
   238  			loopAssign     *ast.AssignStmt // first += assignment within a loop
   239  			seenRvalueUse  bool            // => we've seen the sole final use of s as an rvalue
   240  		)
   241  		for curUse := range index.Uses(v) {
   242  			// Strip enclosing parens around Ident.
   243  			ek, _ := curUse.ParentEdge()
   244  			for ek == edge.ParenExpr_X {
   245  				curUse = curUse.Parent()
   246  				ek, _ = curUse.ParentEdge()
   247  			}
   248  
   249  			// The rvalueUse must be the lexically last use.
   250  			if seenRvalueUse {
   251  				continue nextcand
   252  			}
   253  
   254  			// intervening reports whether cur has an ancestor of
   255  			// one of the given types that is within the scope of v.
   256  			intervening := func(types ...ast.Node) bool {
   257  				for cur := range curUse.Enclosing(types...) {
   258  					if v.Pos() <= cur.Node().Pos() { // in scope of v
   259  						return true
   260  					}
   261  				}
   262  				return false
   263  			}
   264  
   265  			if ek == edge.AssignStmt_Lhs {
   266  				assign := curUse.Parent().Node().(*ast.AssignStmt)
   267  				if assign.Tok != token.ADD_ASSIGN {
   268  					continue nextcand
   269  				}
   270  				// Have: s += expr
   271  
   272  				// At least one of the += operations
   273  				// must appear within a loop.
   274  				// relative to the declaration of s.
   275  				if intervening((*ast.ForStmt)(nil), (*ast.RangeStmt)(nil)) {
   276  					numLoopAssigns++
   277  					if loopAssign == nil {
   278  						loopAssign = assign
   279  					}
   280  				}
   281  
   282  				// s +=          expr
   283  				//  -------------    -
   284  				// s.WriteString(expr)
   285  				edits = append(edits, []analysis.TextEdit{
   286  					// replace += with .WriteString()
   287  					{
   288  						Pos:     assign.TokPos,
   289  						End:     assign.Rhs[0].Pos(),
   290  						NewText: []byte(".WriteString("),
   291  					},
   292  					// insert ")"
   293  					{
   294  						Pos:     assign.End(),
   295  						End:     assign.End(),
   296  						NewText: []byte(")"),
   297  					},
   298  				}...)
   299  
   300  			} else if ek == edge.UnaryExpr_X &&
   301  				curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
   302  				// Have: use(&s)
   303  				continue nextcand // s is used as an lvalue; reject
   304  
   305  			} else {
   306  				// The only possible l-value uses of a string variable
   307  				// are assignments (s=expr, s+=expr, etc) and &s.
   308  				// (For strings, we can ignore method calls s.m().)
   309  				// All other uses are r-values.
   310  				seenRvalueUse = true
   311  
   312  				edits = append(edits, analysis.TextEdit{
   313  					// insert ".String()"
   314  					Pos:     curUse.Node().End(),
   315  					End:     curUse.Node().End(),
   316  					NewText: []byte(".String()"),
   317  				})
   318  			}
   319  		}
   320  		if !seenRvalueUse {
   321  			continue nextcand // no rvalue use; reject
   322  		}
   323  		if numLoopAssigns == 0 {
   324  			continue nextcand // no += in a loop; reject
   325  		}
   326  
   327  		lastEditFile = file
   328  		lastEditEnd = edits[len(edits)-1].End
   329  
   330  		pass.Report(analysis.Diagnostic{
   331  			Pos:     loopAssign.Pos(),
   332  			End:     loopAssign.End(),
   333  			Message: "using string += string in a loop is inefficient",
   334  			SuggestedFixes: []analysis.SuggestedFix{{
   335  				Message:   "Replace string += string with strings.Builder",
   336  				TextEdits: edits,
   337  			}},
   338  		})
   339  	}
   340  
   341  	return nil, nil
   342  }
   343  
   344  // isEmptyString reports whether e (a string-typed expression) has constant value "".
   345  func isEmptyString(info *types.Info, e ast.Expr) bool {
   346  	tv, ok := info.Types[e]
   347  	return ok && tv.Value != nil && constant.StringVal(tv.Value) == ""
   348  }
   349  

View as plain text