honnef.co/go/tools@v0.5.0-0.dev.0.20240520180541-dcae280a5e87/simple/s1025/s1025.go (about)

     1  package s1025
     2  
     3  import (
     4  	"go/ast"
     5  	"go/types"
     6  
     7  	"honnef.co/go/tools/analysis/code"
     8  	"honnef.co/go/tools/analysis/edit"
     9  	"honnef.co/go/tools/analysis/facts/generated"
    10  	"honnef.co/go/tools/analysis/lint"
    11  	"honnef.co/go/tools/analysis/report"
    12  	"honnef.co/go/tools/go/types/typeutil"
    13  	"honnef.co/go/tools/internal/passes/buildir"
    14  	"honnef.co/go/tools/knowledge"
    15  	"honnef.co/go/tools/pattern"
    16  
    17  	"golang.org/x/exp/typeparams"
    18  	"golang.org/x/tools/go/analysis"
    19  	"golang.org/x/tools/go/analysis/passes/inspect"
    20  )
    21  
    22  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    23  	Analyzer: &analysis.Analyzer{
    24  		Name:     "S1025",
    25  		Run:      run,
    26  		Requires: []*analysis.Analyzer{buildir.Analyzer, inspect.Analyzer, generated.Analyzer},
    27  	},
    28  	Doc: &lint.Documentation{
    29  		Title: `Don't use \'fmt.Sprintf("%s", x)\' unnecessarily`,
    30  		Text: `In many instances, there are easier and more efficient ways of getting
    31  a value's string representation. Whenever a value's underlying type is
    32  a string already, or the type has a String method, they should be used
    33  directly.
    34  
    35  Given the following shared definitions
    36  
    37      type T1 string
    38      type T2 int
    39  
    40      func (T2) String() string { return "Hello, world" }
    41  
    42      var x string
    43      var y T1
    44      var z T2
    45  
    46  we can simplify
    47  
    48      fmt.Sprintf("%s", x)
    49      fmt.Sprintf("%s", y)
    50      fmt.Sprintf("%s", z)
    51  
    52  to
    53  
    54      x
    55      string(y)
    56      z.String()
    57  `,
    58  		Since:   "2017.1",
    59  		MergeIf: lint.MergeIfAll,
    60  	},
    61  })
    62  
    63  var Analyzer = SCAnalyzer.Analyzer
    64  
    65  var checkRedundantSprintfQ = pattern.MustParse(`(CallExpr (Symbol "fmt.Sprintf") [format arg])`)
    66  
    67  func run(pass *analysis.Pass) (interface{}, error) {
    68  	fn := func(node ast.Node) {
    69  		m, ok := code.Match(pass, checkRedundantSprintfQ, node)
    70  		if !ok {
    71  			return
    72  		}
    73  
    74  		format := m.State["format"].(ast.Expr)
    75  		arg := m.State["arg"].(ast.Expr)
    76  		// TODO(dh): should we really support named constants here?
    77  		// shouldn't we only look for string literals? to avoid false
    78  		// positives via build tags?
    79  		if s, ok := code.ExprToString(pass, format); !ok || s != "%s" {
    80  			return
    81  		}
    82  		typ := pass.TypesInfo.TypeOf(arg)
    83  		if typeparams.IsTypeParam(typ) {
    84  			return
    85  		}
    86  		irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
    87  
    88  		if typeutil.IsTypeWithName(typ, "reflect.Value") {
    89  			// printing with %s produces output different from using
    90  			// the String method
    91  			return
    92  		}
    93  
    94  		if isFormatter(typ, &irpkg.Prog.MethodSets) {
    95  			// the type may choose to handle %s in arbitrary ways
    96  			return
    97  		}
    98  
    99  		if types.Implements(typ, knowledge.Interfaces["fmt.Stringer"]) {
   100  			replacement := &ast.CallExpr{
   101  				Fun: &ast.SelectorExpr{
   102  					X:   arg,
   103  					Sel: &ast.Ident{Name: "String"},
   104  				},
   105  			}
   106  			report.Report(pass, node, "should use String() instead of fmt.Sprintf",
   107  				report.Fixes(edit.Fix("replace with call to String method", edit.ReplaceWithNode(pass.Fset, node, replacement))))
   108  		} else if types.Unalias(typ) == types.Universe.Lookup("string").Type() {
   109  			report.Report(pass, node, "the argument is already a string, there's no need to use fmt.Sprintf",
   110  				report.FilterGenerated(),
   111  				report.Fixes(edit.Fix("remove unnecessary call to fmt.Sprintf", edit.ReplaceWithNode(pass.Fset, node, arg))))
   112  		} else if typ.Underlying() == types.Universe.Lookup("string").Type() {
   113  			replacement := &ast.CallExpr{
   114  				Fun:  &ast.Ident{Name: "string"},
   115  				Args: []ast.Expr{arg},
   116  			}
   117  			report.Report(pass, node, "the argument's underlying type is a string, should use a simple conversion instead of fmt.Sprintf",
   118  				report.FilterGenerated(),
   119  				report.Fixes(edit.Fix("replace with conversion to string", edit.ReplaceWithNode(pass.Fset, node, replacement))))
   120  		} else if code.IsOfStringConvertibleByteSlice(pass, arg) {
   121  			replacement := &ast.CallExpr{
   122  				Fun:  &ast.Ident{Name: "string"},
   123  				Args: []ast.Expr{arg},
   124  			}
   125  			report.Report(pass, node, "the argument's underlying type is a slice of bytes, should use a simple conversion instead of fmt.Sprintf",
   126  				report.FilterGenerated(),
   127  				report.Fixes(edit.Fix("replace with conversion to string", edit.ReplaceWithNode(pass.Fset, node, replacement))))
   128  		}
   129  
   130  	}
   131  	code.Preorder(pass, fn, (*ast.CallExpr)(nil))
   132  	return nil, nil
   133  }
   134  
   135  func isFormatter(T types.Type, msCache *typeutil.MethodSetCache) bool {
   136  	// TODO(dh): this function also exists in staticcheck/lint.go – deduplicate.
   137  
   138  	ms := msCache.MethodSet(T)
   139  	sel := ms.Lookup(nil, "Format")
   140  	if sel == nil {
   141  		return false
   142  	}
   143  	fn, ok := sel.Obj().(*types.Func)
   144  	if !ok {
   145  		// should be unreachable
   146  		return false
   147  	}
   148  	sig := fn.Type().(*types.Signature)
   149  	if sig.Params().Len() != 2 {
   150  		return false
   151  	}
   152  	// TODO(dh): check the types of the arguments for more
   153  	// precision
   154  	if sig.Results().Len() != 0 {
   155  		return false
   156  	}
   157  	return true
   158  }