github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/simple/s1025/s1025.go (about)

     1  package s1025
     2  
     3  import (
     4  	"go/ast"
     5  	"go/types"
     6  
     7  	"github.com/amarpal/go-tools/analysis/code"
     8  	"github.com/amarpal/go-tools/analysis/edit"
     9  	"github.com/amarpal/go-tools/analysis/facts/generated"
    10  	"github.com/amarpal/go-tools/analysis/lint"
    11  	"github.com/amarpal/go-tools/analysis/report"
    12  	"github.com/amarpal/go-tools/go/types/typeutil"
    13  	"github.com/amarpal/go-tools/internal/passes/buildir"
    14  	"github.com/amarpal/go-tools/knowledge"
    15  	"github.com/amarpal/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 types.TypeString(typ, nil) == "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 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 slice, ok := typ.Underlying().(*types.Slice); ok && slice.Elem() == types.Universe.Lookup("byte").Type() {
   121  			// Note that we check slice.Elem(), not slice.Elem().Underlying, because of https://github.com/golang/go/issues/23536
   122  			replacement := &ast.CallExpr{
   123  				Fun:  &ast.Ident{Name: "string"},
   124  				Args: []ast.Expr{arg},
   125  			}
   126  			report.Report(pass, node, "the argument's underlying type is a slice of bytes, should use a simple conversion instead of fmt.Sprintf",
   127  				report.FilterGenerated(),
   128  				report.Fixes(edit.Fix("replace with conversion to string", edit.ReplaceWithNode(pass.Fset, node, replacement))))
   129  		}
   130  
   131  	}
   132  	code.Preorder(pass, fn, (*ast.CallExpr)(nil))
   133  	return nil, nil
   134  }
   135  
   136  func isFormatter(T types.Type, msCache *typeutil.MethodSetCache) bool {
   137  	// TODO(dh): this function also exists in staticcheck/lint.go – deduplicate.
   138  
   139  	ms := msCache.MethodSet(T)
   140  	sel := ms.Lookup(nil, "Format")
   141  	if sel == nil {
   142  		return false
   143  	}
   144  	fn, ok := sel.Obj().(*types.Func)
   145  	if !ok {
   146  		// should be unreachable
   147  		return false
   148  	}
   149  	sig := fn.Type().(*types.Signature)
   150  	if sig.Params().Len() != 2 {
   151  		return false
   152  	}
   153  	// TODO(dh): check the types of the arguments for more
   154  	// precision
   155  	if sig.Results().Len() != 0 {
   156  		return false
   157  	}
   158  	return true
   159  }