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

View as plain text