golang.org/x/tools@v0.21.0/go/analysis/passes/timeformat/timeformat.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package timeformat defines an Analyzer that checks for the use 6 // of time.Format or time.Parse calls with a bad format. 7 package timeformat 8 9 import ( 10 _ "embed" 11 "go/ast" 12 "go/constant" 13 "go/token" 14 "go/types" 15 "strings" 16 17 "golang.org/x/tools/go/analysis" 18 "golang.org/x/tools/go/analysis/passes/inspect" 19 "golang.org/x/tools/go/analysis/passes/internal/analysisutil" 20 "golang.org/x/tools/go/ast/inspector" 21 "golang.org/x/tools/go/types/typeutil" 22 ) 23 24 const badFormat = "2006-02-01" 25 const goodFormat = "2006-01-02" 26 27 //go:embed doc.go 28 var doc string 29 30 var Analyzer = &analysis.Analyzer{ 31 Name: "timeformat", 32 Doc: analysisutil.MustExtractDoc(doc, "timeformat"), 33 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/timeformat", 34 Requires: []*analysis.Analyzer{inspect.Analyzer}, 35 Run: run, 36 } 37 38 func run(pass *analysis.Pass) (interface{}, error) { 39 // Note: (time.Time).Format is a method and can be a typeutil.Callee 40 // without directly importing "time". So we cannot just skip this package 41 // when !analysisutil.Imports(pass.Pkg, "time"). 42 // TODO(taking): Consider using a prepass to collect typeutil.Callees. 43 44 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 45 46 nodeFilter := []ast.Node{ 47 (*ast.CallExpr)(nil), 48 } 49 inspect.Preorder(nodeFilter, func(n ast.Node) { 50 call := n.(*ast.CallExpr) 51 fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func) 52 if !ok { 53 return 54 } 55 if !isTimeDotFormat(fn) && !isTimeDotParse(fn) { 56 return 57 } 58 if len(call.Args) > 0 { 59 arg := call.Args[0] 60 badAt := badFormatAt(pass.TypesInfo, arg) 61 62 if badAt > -1 { 63 // Check if it's a literal string, otherwise we can't suggest a fix. 64 if _, ok := arg.(*ast.BasicLit); ok { 65 pos := int(arg.Pos()) + badAt + 1 // +1 to skip the " or ` 66 end := pos + len(badFormat) 67 68 pass.Report(analysis.Diagnostic{ 69 Pos: token.Pos(pos), 70 End: token.Pos(end), 71 Message: badFormat + " should be " + goodFormat, 72 SuggestedFixes: []analysis.SuggestedFix{{ 73 Message: "Replace " + badFormat + " with " + goodFormat, 74 TextEdits: []analysis.TextEdit{{ 75 Pos: token.Pos(pos), 76 End: token.Pos(end), 77 NewText: []byte(goodFormat), 78 }}, 79 }}, 80 }) 81 } else { 82 pass.Reportf(arg.Pos(), badFormat+" should be "+goodFormat) 83 } 84 } 85 } 86 }) 87 return nil, nil 88 } 89 90 func isTimeDotFormat(f *types.Func) bool { 91 if f.Name() != "Format" || f.Pkg() == nil || f.Pkg().Path() != "time" { 92 return false 93 } 94 // Verify that the receiver is time.Time. 95 recv := f.Type().(*types.Signature).Recv() 96 return recv != nil && analysisutil.IsNamedType(recv.Type(), "time", "Time") 97 } 98 99 func isTimeDotParse(f *types.Func) bool { 100 return analysisutil.IsFunctionNamed(f, "time", "Parse") 101 } 102 103 // badFormatAt return the start of a bad format in e or -1 if no bad format is found. 104 func badFormatAt(info *types.Info, e ast.Expr) int { 105 tv, ok := info.Types[e] 106 if !ok { // no type info, assume good 107 return -1 108 } 109 110 t, ok := tv.Type.(*types.Basic) // sic, no unalias 111 if !ok || t.Info()&types.IsString == 0 { 112 return -1 113 } 114 115 if tv.Value == nil { 116 return -1 117 } 118 119 return strings.Index(constant.StringVal(tv.Value), badFormat) 120 }