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