github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/simple/s1038/s1038.go (about) 1 package s1038 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/types" 7 8 "github.com/amarpal/go-tools/analysis/code" 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/pattern" 14 15 "golang.org/x/tools/go/analysis" 16 "golang.org/x/tools/go/analysis/passes/inspect" 17 ) 18 19 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ 20 Analyzer: &analysis.Analyzer{ 21 Name: "S1038", 22 Run: run, 23 Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer}, 24 }, 25 Doc: &lint.Documentation{ 26 Title: "Unnecessarily complex way of printing formatted string", 27 Text: `Instead of using \'fmt.Print(fmt.Sprintf(...))\', one can use \'fmt.Printf(...)\'.`, 28 Since: "2020.1", 29 MergeIf: lint.MergeIfAny, 30 }, 31 }) 32 33 var Analyzer = SCAnalyzer.Analyzer 34 35 var ( 36 checkPrintSprintQ = pattern.MustParse(` 37 (Or 38 (CallExpr 39 fn@(Or 40 (Symbol "fmt.Print") 41 (Symbol "fmt.Sprint") 42 (Symbol "fmt.Println") 43 (Symbol "fmt.Sprintln")) 44 [(CallExpr (Symbol "fmt.Sprintf") f:_)]) 45 (CallExpr 46 fn@(Or 47 (Symbol "fmt.Fprint") 48 (Symbol "fmt.Fprintln")) 49 [_ (CallExpr (Symbol "fmt.Sprintf") f:_)]))`) 50 51 checkTestingErrorSprintfQ = pattern.MustParse(` 52 (CallExpr 53 sel@(SelectorExpr 54 recv 55 (Ident 56 name@(Or 57 "Error" 58 "Fatal" 59 "Fatalln" 60 "Log" 61 "Panic" 62 "Panicln" 63 "Print" 64 "Println" 65 "Skip"))) 66 [(CallExpr (Symbol "fmt.Sprintf") args)])`) 67 68 checkLogSprintfQ = pattern.MustParse(` 69 (CallExpr 70 (Symbol 71 (Or 72 "log.Fatal" 73 "log.Fatalln" 74 "log.Panic" 75 "log.Panicln" 76 "log.Print" 77 "log.Println")) 78 [(CallExpr (Symbol "fmt.Sprintf") args)])`) 79 80 checkSprintfMapping = map[string]struct { 81 recv string 82 alternative string 83 }{ 84 "(*testing.common).Error": {"(*testing.common)", "Errorf"}, 85 "(testing.TB).Error": {"(testing.TB)", "Errorf"}, 86 "(*testing.common).Fatal": {"(*testing.common)", "Fatalf"}, 87 "(testing.TB).Fatal": {"(testing.TB)", "Fatalf"}, 88 "(*testing.common).Log": {"(*testing.common)", "Logf"}, 89 "(testing.TB).Log": {"(testing.TB)", "Logf"}, 90 "(*testing.common).Skip": {"(*testing.common)", "Skipf"}, 91 "(testing.TB).Skip": {"(testing.TB)", "Skipf"}, 92 "(*log.Logger).Fatal": {"(*log.Logger)", "Fatalf"}, 93 "(*log.Logger).Fatalln": {"(*log.Logger)", "Fatalf"}, 94 "(*log.Logger).Panic": {"(*log.Logger)", "Panicf"}, 95 "(*log.Logger).Panicln": {"(*log.Logger)", "Panicf"}, 96 "(*log.Logger).Print": {"(*log.Logger)", "Printf"}, 97 "(*log.Logger).Println": {"(*log.Logger)", "Printf"}, 98 "log.Fatal": {"", "log.Fatalf"}, 99 "log.Fatalln": {"", "log.Fatalf"}, 100 "log.Panic": {"", "log.Panicf"}, 101 "log.Panicln": {"", "log.Panicf"}, 102 "log.Print": {"", "log.Printf"}, 103 "log.Println": {"", "log.Printf"}, 104 } 105 ) 106 107 func run(pass *analysis.Pass) (interface{}, error) { 108 fmtPrintf := func(node ast.Node) { 109 m, ok := code.Match(pass, checkPrintSprintQ, node) 110 if !ok { 111 return 112 } 113 114 name := m.State["fn"].(*types.Func).Name() 115 var msg string 116 switch name { 117 case "Print", "Fprint", "Sprint": 118 newname := name + "f" 119 msg = fmt.Sprintf("should use fmt.%s instead of fmt.%s(fmt.Sprintf(...))", newname, name) 120 case "Println", "Fprintln", "Sprintln": 121 if _, ok := m.State["f"].(*ast.BasicLit); !ok { 122 // This may be an instance of 123 // fmt.Println(fmt.Sprintf(arg, ...)) where arg is an 124 // externally provided format string and the caller 125 // cannot guarantee that the format string ends with a 126 // newline. 127 return 128 } 129 newname := name[:len(name)-2] + "f" 130 msg = fmt.Sprintf("should use fmt.%s instead of fmt.%s(fmt.Sprintf(...)) (but don't forget the newline)", newname, name) 131 } 132 report.Report(pass, node, msg, 133 report.FilterGenerated()) 134 } 135 136 methSprintf := func(node ast.Node) { 137 m, ok := code.Match(pass, checkTestingErrorSprintfQ, node) 138 if !ok { 139 return 140 } 141 mapped, ok := checkSprintfMapping[code.CallName(pass, node.(*ast.CallExpr))] 142 if !ok { 143 return 144 } 145 146 // Ensure that Errorf/Fatalf refer to the right method 147 recvTV, ok := pass.TypesInfo.Types[m.State["recv"].(ast.Expr)] 148 if !ok { 149 return 150 } 151 obj, _, _ := types.LookupFieldOrMethod(recvTV.Type, recvTV.Addressable(), nil, mapped.alternative) 152 f, ok := obj.(*types.Func) 153 if !ok { 154 return 155 } 156 if typeutil.FuncName(f) != mapped.recv+"."+mapped.alternative { 157 return 158 } 159 160 alt := &ast.SelectorExpr{ 161 X: m.State["recv"].(ast.Expr), 162 Sel: &ast.Ident{Name: mapped.alternative}, 163 } 164 report.Report(pass, node, fmt.Sprintf("should use %s(...) instead of %s(fmt.Sprintf(...))", report.Render(pass, alt), report.Render(pass, m.State["sel"].(*ast.SelectorExpr)))) 165 } 166 167 pkgSprintf := func(node ast.Node) { 168 _, ok := code.Match(pass, checkLogSprintfQ, node) 169 if !ok { 170 return 171 } 172 callName := code.CallName(pass, node.(*ast.CallExpr)) 173 mapped, ok := checkSprintfMapping[callName] 174 if !ok { 175 return 176 } 177 report.Report(pass, node, fmt.Sprintf("should use %s(...) instead of %s(fmt.Sprintf(...))", mapped.alternative, callName)) 178 } 179 180 fn := func(node ast.Node) { 181 fmtPrintf(node) 182 // TODO(dh): add suggested fixes 183 methSprintf(node) 184 pkgSprintf(node) 185 } 186 code.Preorder(pass, fn, (*ast.CallExpr)(nil)) 187 return nil, nil 188 }