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 }