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 }