Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/modernize/fmtappendf.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/types"
    12  	"strings"
    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/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/fmtstr"
    21  	"golang.org/x/tools/internal/typesinternal/typeindex"
    22  	"golang.org/x/tools/internal/versions"
    23  )
    24  
    25  var FmtAppendfAnalyzer = &analysis.Analyzer{
    26  	Name: "fmtappendf",
    27  	Doc:  analyzerutil.MustExtractDoc(doc, "fmtappendf"),
    28  	Requires: []*analysis.Analyzer{
    29  		inspect.Analyzer,
    30  		typeindexanalyzer.Analyzer,
    31  	},
    32  	Run: fmtappendf,
    33  	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#fmtappendf",
    34  }
    35  
    36  // The fmtappend function replaces []byte(fmt.Sprintf(...)) by
    37  // fmt.Appendf(nil, ...), and similarly for Sprint, Sprintln.
    38  func fmtappendf(pass *analysis.Pass) (any, error) {
    39  	index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
    40  	for _, fn := range []types.Object{
    41  		index.Object("fmt", "Sprintf"),
    42  		index.Object("fmt", "Sprintln"),
    43  		index.Object("fmt", "Sprint"),
    44  	} {
    45  		for curCall := range index.Calls(fn) {
    46  			call := curCall.Node().(*ast.CallExpr)
    47  			if ek, idx := curCall.ParentEdge(); ek == edge.CallExpr_Args && idx == 0 {
    48  				// Is parent a T(fmt.SprintX(...)) conversion?
    49  				conv := curCall.Parent().Node().(*ast.CallExpr)
    50  				info := pass.TypesInfo
    51  				tv := info.Types[conv.Fun]
    52  				if tv.IsType() && types.Identical(tv.Type, byteSliceType) {
    53  					// Have: []byte(fmt.SprintX(...))
    54  					if len(call.Args) == 0 {
    55  						continue
    56  					}
    57  					// fmt.Sprint(f) and fmt.Append(f) have different nil semantics
    58  					// when the format produces an empty string:
    59  					// []byte(fmt.Sprintf("")) returns an empty but non-nil
    60  					// []byte{}, while fmt.Appendf(nil, "") returns nil) so we
    61  					// should skip these cases.
    62  					if fn.Name() == "Sprint" || fn.Name() == "Sprintf" {
    63  						format := info.Types[call.Args[0]].Value
    64  						if format != nil && mayFormatEmpty(constant.StringVal(format)) {
    65  							continue
    66  						}
    67  					}
    68  
    69  					// Find "Sprint" identifier.
    70  					var id *ast.Ident
    71  					switch e := ast.Unparen(call.Fun).(type) {
    72  					case *ast.SelectorExpr:
    73  						id = e.Sel // "fmt.Sprint"
    74  					case *ast.Ident:
    75  						id = e // "Sprint" after `import . "fmt"`
    76  					}
    77  
    78  					old, new := fn.Name(), strings.Replace(fn.Name(), "Sprint", "Append", 1)
    79  					edits := []analysis.TextEdit{
    80  						{
    81  							// Delete "[]byte(", including any spaces before the first argument.
    82  							Pos: conv.Pos(),
    83  							End: conv.Args[0].Pos(), // always exactly one argument in a valid byte slice conversion
    84  						},
    85  						{
    86  							// Delete ")", including any non-args (space or
    87  							// commas) that come before the right parenthesis.
    88  							// Leaving an extra comma here produces invalid
    89  							// code. (See golang/go#74709)
    90  							// Unfortunately, this and the edit above may result
    91  							// in deleting some comments.
    92  							Pos: conv.Args[0].End(),
    93  							End: conv.Rparen + 1,
    94  						},
    95  						{
    96  							Pos:     id.Pos(),
    97  							End:     id.End(),
    98  							NewText: []byte(new),
    99  						},
   100  						{
   101  							Pos:     call.Lparen + 1,
   102  							NewText: []byte("nil, "),
   103  						},
   104  					}
   105  					if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_19) {
   106  						continue
   107  					}
   108  					pass.Report(analysis.Diagnostic{
   109  						Pos:     conv.Pos(),
   110  						End:     conv.End(),
   111  						Message: fmt.Sprintf("Replace []byte(fmt.%s...) with fmt.%s", old, new),
   112  						SuggestedFixes: []analysis.SuggestedFix{{
   113  							Message:   fmt.Sprintf("Replace []byte(fmt.%s...) with fmt.%s", old, new),
   114  							TextEdits: edits,
   115  						}},
   116  					})
   117  				}
   118  			}
   119  		}
   120  	}
   121  	return nil, nil
   122  }
   123  
   124  // mayFormatEmpty reports whether fmt.Sprintf might produce an empty string.
   125  // It returns false in the following two cases:
   126  //  1. formatStr contains non-operation characters.
   127  //  2. formatStr contains formatting verbs besides s, v, x, X (verbs which may
   128  //     produce empty results)
   129  //
   130  // In all other cases it returns true.
   131  func mayFormatEmpty(formatStr string) bool {
   132  	if formatStr == "" {
   133  		return true
   134  	}
   135  	operations, err := fmtstr.Parse(formatStr, 0)
   136  	if err != nil {
   137  		// If formatStr is malformed, the printf analyzer will report a
   138  		// diagnostic, so we can ignore this error.
   139  		// Calling Parse on a string without % formatters also returns an error,
   140  		// in which case we can safely return false.
   141  		return false
   142  	}
   143  	totalOpsLen := 0
   144  	for _, op := range operations {
   145  		totalOpsLen += len(op.Text)
   146  		if !strings.ContainsRune("svxX", rune(op.Verb.Verb)) && op.Prec.Fixed != 0 {
   147  			// A non [s, v, x, X] formatter with non-zero precision cannot
   148  			// produce an empty string.
   149  			return false
   150  		}
   151  	}
   152  	// If the format string contains non-operation characters, it cannot produce
   153  	// the empty string.
   154  	if totalOpsLen != len(formatStr) {
   155  		return false
   156  	}
   157  	// If we get here, it means that all formatting verbs are %s, %v, %x, %X,
   158  	// and there are no additional non-operation characters. We conservatively
   159  	// report that this may format as an empty string, ignoring uses of
   160  	// precision and the values of the formatter args.
   161  	return true
   162  }
   163  

View as plain text