github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/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 "go/ast" 11 "go/constant" 12 "go/token" 13 "go/types" 14 "strings" 15 16 "golang.org/x/tools/go/analysis" 17 "golang.org/x/tools/go/analysis/passes/inspect" 18 "golang.org/x/tools/go/ast/inspector" 19 "golang.org/x/tools/go/types/typeutil" 20 ) 21 22 const badFormat = "2006-02-01" 23 const goodFormat = "2006-01-02" 24 25 const Doc = `check for calls of (time.Time).Format or time.Parse with 2006-02-01 26 27 The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm) 28 format. Internationally, "yyyy-dd-mm" does not occur in common calendar date 29 standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended. 30 ` 31 32 var Analyzer = &analysis.Analyzer{ 33 Name: "timeformat", 34 Doc: Doc, 35 Requires: []*analysis.Analyzer{inspect.Analyzer}, 36 Run: run, 37 } 38 39 func run(pass *analysis.Pass) (interface{}, error) { 40 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 41 42 nodeFilter := []ast.Node{ 43 (*ast.CallExpr)(nil), 44 } 45 inspect.Preorder(nodeFilter, func(n ast.Node) { 46 call := n.(*ast.CallExpr) 47 fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func) 48 if !ok { 49 return 50 } 51 if !isTimeDotFormat(fn) && !isTimeDotParse(fn) { 52 return 53 } 54 if len(call.Args) > 0 { 55 arg := call.Args[0] 56 badAt := badFormatAt(pass.TypesInfo, arg) 57 58 if badAt > -1 { 59 // Check if it's a literal string, otherwise we can't suggest a fix. 60 if _, ok := arg.(*ast.BasicLit); ok { 61 pos := int(arg.Pos()) + badAt + 1 // +1 to skip the " or ` 62 end := pos + len(badFormat) 63 64 pass.Report(analysis.Diagnostic{ 65 Pos: token.Pos(pos), 66 End: token.Pos(end), 67 Message: badFormat + " should be " + goodFormat, 68 SuggestedFixes: []analysis.SuggestedFix{{ 69 Message: "Replace " + badFormat + " with " + goodFormat, 70 TextEdits: []analysis.TextEdit{{ 71 Pos: token.Pos(pos), 72 End: token.Pos(end), 73 NewText: []byte(goodFormat), 74 }}, 75 }}, 76 }) 77 } else { 78 pass.Reportf(arg.Pos(), badFormat+" should be "+goodFormat) 79 } 80 } 81 } 82 }) 83 return nil, nil 84 } 85 86 func isTimeDotFormat(f *types.Func) bool { 87 if f.Name() != "Format" || f.Pkg().Path() != "time" { 88 return false 89 } 90 sig, ok := f.Type().(*types.Signature) 91 if !ok { 92 return false 93 } 94 // Verify that the receiver is time.Time. 95 recv := sig.Recv() 96 if recv == nil { 97 return false 98 } 99 named, ok := recv.Type().(*types.Named) 100 return ok && named.Obj().Name() == "Time" 101 } 102 103 func isTimeDotParse(f *types.Func) bool { 104 if f.Name() != "Parse" || f.Pkg().Path() != "time" { 105 return false 106 } 107 // Verify that there is no receiver. 108 sig, ok := f.Type().(*types.Signature) 109 return ok && sig.Recv() == nil 110 } 111 112 // badFormatAt return the start of a bad format in e or -1 if no bad format is found. 113 func badFormatAt(info *types.Info, e ast.Expr) int { 114 tv, ok := info.Types[e] 115 if !ok { // no type info, assume good 116 return -1 117 } 118 119 t, ok := tv.Type.(*types.Basic) 120 if !ok || t.Info()&types.IsString == 0 { 121 return -1 122 } 123 124 if tv.Value == nil { 125 return -1 126 } 127 128 return strings.Index(constant.StringVal(tv.Value), badFormat) 129 }